Repository navigation
Expand file tree
/
Copy pathepiphany_20260909.html
More file actions
1065 lines (1002 loc) · 393 KB
/
Copy pathepiphany_20260909.html
File metadata and controls
1065 lines (1002 loc) · 393 KB
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
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="dark">
<title>Code Audit — Interactive</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{color-scheme:dark}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0f172a;color:#e5e7eb;font-size:14px;line-height:1.5}
.app{max-width:1100px;margin:0 auto;padding:1.5rem 1rem}
.top{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;margin-bottom:1rem;flex-wrap:wrap}
h1{font-size:1.5rem;font-weight:700;color:#f1f5f9}
.meta{font-size:.8rem;color:#6b7280;margin-top:.2rem}
.top-right{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}
.project-info{margin-bottom:1rem;padding:.75rem .9rem;background:#1e293b;border:1px solid #334155;border-radius:8px}
.project-info .pi-name{font-size:.95rem;font-weight:700;color:#f1f5f9}
.project-info .pi-desc{font-size:.82rem;color:#d1d5db;margin-top:.25rem}
.project-info .pi-row{display:flex;flex-wrap:wrap;gap:.4rem .9rem;margin-top:.45rem;font-size:.78rem;color:#9ca3af}
.project-info .pi-row .pi-k{color:#6b7280}
.project-info a{color:#60a5fa;text-decoration:none}
.project-info a:hover{text-decoration:underline}
.project-info code{font-family:"JetBrains Mono","Fira Code",Consolas,monospace;font-size:.74rem;color:#93c5fd}
.pi-repo-btn{display:inline-flex;align-items:center;gap:.3rem;padding:.18rem .5rem;background:#0f172a;border:1px solid #3b82f6;border-radius:6px;color:#60a5fa !important;font-weight:600}
.pi-repo-btn:hover{background:#1d283a;text-decoration:none !important}
#search{background:#1e293b;border:1px solid #334155;color:#e5e7eb;padding:.4rem .75rem;border-radius:6px;font-size:.875rem;width:220px;outline:none}
#search:focus{border-color:#3b82f6}
.group-label{font-size:.8rem;color:#9ca3af;display:flex;align-items:center;gap:.4rem}
select{background:#1e293b;border:1px solid #334155;color:#e5e7eb;padding:.35rem .6rem;border-radius:6px;font-size:.8rem;outline:none;cursor:pointer}
select:focus{border-color:#3b82f6}
.filterbar{display:flex;flex-direction:column;gap:.4rem;margin-bottom:.6rem;padding:.6rem .75rem;background:#1e293b;border:1px solid #334155;border-radius:8px}
.filter-row{display:flex;flex-wrap:wrap;gap:.3rem;align-items:center}
.filter-lbl{font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#4b5563;white-space:nowrap;margin-right:.25rem}
.filter-toggle{font-size:.65rem;font-weight:600;padding:.18rem .5rem;border-radius:4px;border:1px solid #334155;background:#0f172a;color:#9ca3af;cursor:pointer;margin-left:auto;white-space:nowrap;letter-spacing:.04em}
.filter-toggle:hover{background:#1e293b;color:#e5e7eb;border-color:#4b5563}
.badge{font-size:.68rem;font-weight:700;padding:.18rem .5rem;border-radius:4px;border:1px solid transparent;cursor:pointer;letter-spacing:.03em;transition:opacity .12s,filter .12s;user-select:none}
.badge.off{opacity:.28;filter:grayscale(.5)}
.sev-critical{background:#450a0a;color:#fca5a5;border-color:#991b1b}
.sev-high {background:#431407;color:#fdba74;border-color:#9a3412}
.sev-medium {background:#422006;color:#fcd34d;border-color:#854d0e}
.sev-low {background:#0c1a3a;color:#93c5fd;border-color:#1d4ed8}
.sev-negligible{background:#1e293b;color:#6b7280;border-color:#334155}
.type-badge{background:#0c1e33;color:#7dd3fc;border-color:#0369a1}
.diff-very-easy{background:#052e16;color:#86efac;border-color:#16a34a}
.diff-easy{background:#064e3b;color:#6ee7b7;border-color:#059669}
.diff-medium{background:#1e293b;color:#9ca3af;border-color:#4b5563}
.diff-hard{background:#2e1065;color:#c4b5fd;border-color:#7c3aed}
.st-open{background:#1e293b;color:#4b5563;border-color:#334155}
.st-fixed{background:#052e16;color:#4ade80;border-color:#16a34a}
.st-wont{background:#431407;color:#fdba74;border-color:#9a3412}
.st-fp{background:#2e1065;color:#c4b5fd;border-color:#7c3aed}
#progress-wrap{display:flex;align-items:center;gap:.75rem;margin-bottom:.6rem;flex-wrap:wrap}
#progress-bar{flex:1;min-width:120px;height:7px;background:#1e293b;border-radius:4px;overflow:hidden;border:1px solid #334155}
#progress-fill{height:100%;background:#22c55e;border-radius:4px;transition:width .35s ease;width:0}
#progress-text{font-size:.75rem;color:#6b7280;white-space:nowrap}
.action-btn{font-size:.72rem;padding:.25rem .6rem;border-radius:5px;border:1px solid #334155;background:#1e293b;color:#9ca3af;cursor:pointer;white-space:nowrap}
.action-btn:hover{background:#334155;color:#e5e7eb}
.action-btn.active{background:#1d4ed8;color:#fff;border-color:#2563eb}
.action-btn.active:hover{background:#2563eb}
.search-wrap{display:flex;align-items:center;gap:.35rem}
#hidden-bar{margin-bottom:.6rem}
.hidden-inner{display:flex;flex-wrap:wrap;gap:.35rem;align-items:center}
.hidden-lbl{font-size:.72rem;color:#4b5563}
.show-btn{font-size:.7rem;padding:.15rem .45rem;border-radius:4px;background:#1e293b;color:#9ca3af;border:1px solid #334155;cursor:pointer}
.show-btn:hover{background:#334155;color:#e5e7eb}
.show-all-btn{font-size:.7rem;padding:.15rem .45rem;border-radius:4px;background:#2563eb;color:#fff;border:none;cursor:pointer}
.show-all-btn:hover{background:#1d4ed8}
.group-section{margin-bottom:1.5rem}
.group-header{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem;padding-bottom:.35rem;border-bottom:1px solid #334155}
.group-title{font-size:.95rem;font-weight:700;color:#d1d5db}
.group-title.all-done{color:#4ade80}
.group-count{font-size:.72rem;color:#4b5563;background:#1e293b;padding:.1rem .4rem;border-radius:10px;white-space:nowrap}
.icon-btn{background:transparent;border:none;color:#334155;cursor:pointer;font-size:.85rem;padding:.05rem .2rem;line-height:1}
.icon-btn:hover{color:#6b7280}
.hide-btn{margin-left:auto;font-size:.68rem;padding:.12rem .45rem;border-radius:4px;background:transparent;color:#334155;border:1px solid #1e293b;cursor:pointer}
.hide-btn:hover{background:#1e293b;color:#6b7280}
.card{background:#1e293b;border:1px solid #334155;border-radius:8px;margin-bottom:.6rem;overflow:hidden;transition:opacity .15s}
.card.resolved{opacity:.55;border-left:3px solid #16a34a}
.card.resolved:hover{opacity:.85}
.card-header{display:flex;align-items:center;gap:.5rem;padding:.45rem .85rem;background:#162032;border-bottom:1px solid #334155;flex-wrap:wrap;cursor:pointer;user-select:none}
.card-header:hover{background:#1a2840}
.card-arrow{font-size:.65rem;color:#334155;flex-shrink:0}
.card-id{font-size:.75rem;font-weight:700;color:#6b7280;font-family:monospace;letter-spacing:.04em}
.card-loc{font-size:.72rem;color:#6b7280;font-family:"JetBrains Mono","Fira Code",Consolas,monospace;letter-spacing:.02em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.status-btn{margin-left:auto;font-size:.68rem;font-weight:600;padding:.15rem .5rem;border-radius:4px;border:1px solid transparent;cursor:pointer;white-space:nowrap;transition:filter .1s}
.status-btn:hover{filter:brightness(1.25)}
.copy-btn{font-size:.68rem;font-weight:600;padding:.15rem .5rem;border-radius:4px;border:1px solid #334155;background:#0f172a;color:#9ca3af;cursor:pointer;white-space:nowrap;transition:background .12s,color .12s}
.copy-btn:hover{background:#334155;color:#e5e7eb}
.card-body{padding:.85rem .9rem}
.field-lbl{font-size:.85rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:#7dd3fc;margin:1rem 0 .35rem;padding-bottom:.18rem;border-bottom:1px solid #1e293b}
.field-lbl:first-child{margin-top:0}
.desc,.fix-txt{font-size:.875rem;color:#d1d5db;line-height:1.55}
.desc p,.fix-txt p{margin:.4rem 0}
.desc p:first-child,.fix-txt p:first-child{margin-top:0}
.desc p:last-child,.fix-txt p:last-child{margin-bottom:0}
.loc-block{margin:.3rem 0 .6rem}
.loc-ref{font-size:.8rem;color:#38bdf8;font-family:"JetBrains Mono","Fira Code",Consolas,monospace;font-weight:600;margin-bottom:.22rem}
a.loc-link{color:inherit;text-decoration:none}
a.loc-link:hover{text-decoration:underline}
.loc-lines{color:#4b5563;font-weight:400}
pre{background:#0f172a;color:#cdd6f4;border-radius:5px;padding:.6rem .8rem;overflow-x:auto;white-space:pre;font-size:.8rem;line-height:1.55;font-family:"JetBrains Mono","Fira Code",Consolas,monospace;border:1px solid #1e293b;max-height:320px}
.desc pre.md-code,.fix-txt pre.md-code{margin:.5rem 0;font-size:.82rem;color:#e5e7eb;background:#0b1224;border:1px solid #1e293b;max-height:none}
code.md-inline{font-family:"JetBrains Mono","Fira Code",Consolas,monospace;font-size:.86em;background:#0b1224;color:#fca5a5;border:1px solid #1e293b;border-radius:3px;padding:.04rem .32rem;white-space:pre-wrap;word-break:break-word}
.not-found{font-size:.8rem;color:#334155;font-style:italic}
mark{background:#854d0e;color:#fef9c3;border-radius:2px;padding:0 .1rem}
.no-results{text-align:center;color:#334155;padding:3rem 1rem;font-size:.9rem}
</style>
</head>
<body>
<script type="application/json" id="D">[
{"id":"PANIC_1","type":"panic","type_label":"Panic","severity":"high","difficulty":"easy","description":"g_variant_parse() can return NULL when the input string is not valid GVariant text. The input comes from the untrusted web process via jsc_value_to_string. The return value is never checked and g_variant_get() is called on it immediately, causing a NULL deref. A malformed web-process-extension message can crash the main browser process.","fix":"Check `if (!variant) return;` after g_variant_parse before calling g_variant_get.","locations":[{"ref":"embed/ephy-embed-shell.c:220-223","code":" 220 | message_str = jsc_value_to_string (message);\n 221 | variant = g_variant_parse (G_VARIANT_TYPE (\"(tb)\"), message_str, NULL, NULL, NULL);\n 222 | \n 223 | g_variant_get (variant, \"(tb)\", &page_id, &insecure_form_action);","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-embed-shell.c#L220-223"}]},
{"id":"PANIC_2","type":"panic","type_label":"Panic","severity":"high","difficulty":"medium","description":"The 'load-failed' signal callback declares its failing_uri parameter as 'const char failing_uri' (a single char) instead of 'const char *failing_uri'. WebKitWebView::load-failed passes a const gchar* in that slot. With the wrong-sized parameter, the C calling convention shifts all following arguments (GError *error, GTask *task), so 'error' and 'task' receive garbage, leading to a crash / use of an invalid GTask pointer whenever a load fails during snapshotting.","fix":"Change the parameter to 'const char *failing_uri'.","locations":[{"ref":"lib/ephy-snapshot-service.c:453-459","code":" 453 | static gboolean\n 454 | webview_load_failed_cb (WebKitWebView *web_view,\n 455 | WebKitLoadEvent load_event,\n 456 | const char failing_uri,\n 457 | GError *error,\n 458 | GTask *task)\n 459 | {","lines":7,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/ephy-snapshot-service.c#L453-459"}]},
{"id":"PANIC_3","type":"panic","type_label":"Panic","severity":"high","difficulty":"medium","description":"web_extension_get_translation() uses raw json-glib accessors on untrusted _locales/<locale>/messages.json: json_node_get_object(root) asserts if root is not an object, json_object_get_object_member(root_object, key) asserts if 'key' is present but not an object, and json_object_get_string_member(name, \"message\") asserts if 'message' is missing or not a string. A malformed messages.json crashes the browser.","fix":"Use the safe helpers: ephy_json_node_get_object / ephy_json_object_get_object / ephy_json_object_get_string and handle NULL.","locations":[{"ref":"src/webextension/ephy-web-extension.c:501-509","code":" 501 | root_object = json_node_get_object (root);\n 502 | if (!root_object) {\n 503 | g_warning (\"WebExtension translation root object is NULL, return NULL.\");\n 504 | return NULL;\n 505 | }\n 506 | \n 507 | name = json_object_get_object_member (root_object, key);\n 508 | if (name)\n 509 | return g_strdup (json_object_get_string_member (name, \"message\"));","lines":9,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension.c#L501-509"}]},
{"id":"PANIC_4","type":"panic","type_label":"Panic","severity":"medium","difficulty":"easy","description":"display_download_finished_notification calls gtk_window_is_active(toplevel) without NULL-checking toplevel; gtk_application_get_active_window can return NULL, and gtk_window_is_active(NULL) crashes.","fix":"Guard: if (toplevel && !gtk_window_is_active(toplevel) && dest) { ... }","locations":[{"ref":"embed/ephy-download.c:970-974","code":" 970 | application = G_APPLICATION (ephy_embed_shell_get_default ());\n 971 | toplevel = gtk_application_get_active_window (GTK_APPLICATION (application));\n 972 | dest = webkit_download_get_destination (download);\n 973 | \n 974 | if (!gtk_window_is_active (toplevel) && dest) {","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-download.c#L970-974"}]},
{"id":"PANIC_5","type":"panic","type_label":"Panic","severity":"medium","difficulty":"easy","description":"download_file_monitor_changed compares g_file_peek_path(file) against webkit_download_get_destination(download->download) with strcmp; the destination can be NULL, and strcmp(path, NULL) crashes.","fix":"const char *dest = webkit_download_get_destination(...); if (!dest || strcmp(g_file_peek_path(file), dest) != 0) return;","locations":[{"ref":"embed/ephy-download.c:993-1008","code":" 993 | static void\n 994 | download_file_monitor_changed (GFileMonitor *monitor,\n 995 | GFile *file,\n 996 | GFile *other_file,\n 997 | GFileMonitorEvent event_type,\n 998 | EphyDownload *download)\n 999 | {\n 1000 | /* Skip messages for <file>.wkdownload */\n 1001 | if (strcmp (g_file_peek_path (file), webkit_download_get_destination (download->download)) != 0)\n 1002 | return;\n 1003 | \n 1004 | download->was_moved = TRUE;\n 1005 | \n 1006 | if (event_type == G_FILE_MONITOR_EVENT_DELETED || event_type == G_FILE_MONITOR_EVENT_MOVED)\n 1007 | g_signal_emit (download, signals[MOVED], 0);\n 1008 | }","lines":16,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-download.c#L993-1008"}]},
{"id":"PANIC_6","type":"panic","type_label":"Panic","severity":"medium","difficulty":"medium","description":"ephy_web_view_load_url builds decoded_url for javascript: URIs via g_uri_parse then g_uri_to_string without checking g_uri_parse for NULL (the GError is discarded). A malformed javascript: URI yields NULL and g_uri_to_string(NULL) is undefined behavior.","fix":"Check uri != NULL after g_uri_parse; skip evaluation on failure.","locations":[{"ref":"embed/ephy-web-view.c:2932-2938","code":" 2932 | if (g_str_has_prefix (effective_url, \"javascript:\")) {\n 2933 | g_autoptr (GUri) uri = NULL;\n 2934 | g_autofree char *decoded_url = NULL;\n 2935 | \n 2936 | uri = g_uri_parse (effective_url, G_URI_FLAGS_PARSE_RELAXED, NULL);\n 2937 | decoded_url = g_uri_to_string (uri);\n 2938 | webkit_web_view_evaluate_javascript (WEBKIT_WEB_VIEW (view), decoded_url, -1, NULL, NULL, NULL, NULL, NULL);","lines":7,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-web-view.c#L2932-2938"}]},
{"id":"PANIC_7","type":"panic","type_label":"Panic","severity":"medium","difficulty":"medium","description":"gnome_get_country_from_locale() appends translated_territory directly with g_string_append() without a NULL check, but get_translated_territory() can return NULL (e.g. unknown territory code, or newlocale() failing for the translation). The sibling gnome_get_language_from_locale() does guard its translated_language against NULL and goto out; the country variant does not. Passing NULL to g_string_append() triggers a GLib assertion/crash.","fix":"Guard with 'if (translated_territory == NULL) goto out;' before appending, mirroring the language version.","locations":[{"ref":"lib/contrib/gnome-languages.c:1192-1193","code":" 1192 | translated_territory = get_translated_territory (territory_code, translation);\n 1193 | g_string_append (full_name, translated_territory);","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/contrib/gnome-languages.c#L1192-1193"}]},
{"id":"PANIC_8","type":"panic","type_label":"Panic","severity":"medium","difficulty":"medium","description":"If g_file_read() fails with an error other than G_IO_ERROR_NOT_FOUND (e.g. permission denied), 'stream' is NULL but execution continues to g_data_input_stream_new(G_INPUT_STREAM(stream)) and later reads, dereferencing NULL. The GError is also leaked in that case (it is only freed in the NOT_FOUND branch).","fix":"After g_file_read(), check 'if (!stream) { g_clear_error (&error); return; }' before using the stream; free/clear the error in all error paths.","locations":[{"ref":"lib/ephy-smaps.c:223-233","code":" 223 | stream = g_file_read (file, NULL, &error);\n 224 | g_object_unref (file);\n 225 | \n 226 | if (error && error->code == G_IO_ERROR_NOT_FOUND) {\n 227 | /* This is not GNU/Linux, do nothing. */\n 228 | g_error_free (error);\n 229 | return;\n 230 | }\n 231 | \n 232 | data_stream = g_data_input_stream_new (G_INPUT_STREAM (stream));\n 233 | g_object_unref (stream);","lines":11,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/ephy-smaps.c#L223-233"}]},
{"id":"PANIC_9","type":"panic","type_label":"Panic","severity":"medium","difficulty":"easy","description":"ephy_sync_debug_load_secrets() dereferences result->data after secret_service_search_sync() without checking that result is non-NULL. The search can succeed (no error) yet return an empty (NULL) list when no matching secret exists, causing a NULL dereference. secret_item_get_secret(result->data) can also return NULL, which is then passed to secret_value_get_text via json_from_string.","fix":"After the error check, add `if (!result) goto free_attributes;` and null-check the SecretValue before use.","locations":[{"ref":"lib/sync/debug/ephy-sync-debug.c:60-67","code":" 60 | if (error) {\n 61 | LOG (\"Error searching sync secrets: %s\", error->message);\n 62 | g_error_free (error);\n 63 | goto free_attributes;\n 64 | }\n 65 | \n 66 | value = secret_item_get_secret (result->data);\n 67 | node = json_from_string (secret_value_get_text (value), &error);","lines":8,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/debug/ephy-sync-debug.c#L60-67"}]},
{"id":"PANIC_10","type":"panic","type_label":"Panic","severity":"medium","difficulty":"easy","description":"In the Chrome importer, secure_origin = ephy_uri_to_security_origin(origin) can return NULL (the helper is documented as possibly NULL). It is then passed unchecked to ephy_password_manager_find()/ephy_password_manager_save(); the NULL target_origin fallback g_strdup(secure_origin) also propagates NULL. The CSV path validates the parsed host, but the Chrome path does not.","fix":"After computing secure_origin, add `if (!secure_origin) continue;` before using it.","locations":[{"ref":"lib/sync/ephy-password-import.c:192-214","code":" 192 | secure_origin = ephy_uri_to_security_origin (origin);\n 193 | \n 194 | secure_target_origin = ephy_uri_to_security_origin (target_origin);\n 195 | \n 196 | if (!secure_target_origin)\n 197 | secure_target_origin = g_strdup (secure_origin);\n 198 | \n 199 | exists = ephy_password_manager_find (manager,\n 200 | secure_origin,\n 201 | secure_target_origin,\n 202 | username,\n 203 | username_field,\n 204 | password_field);\n 205 | \n 206 | ephy_password_manager_save (manager,\n 207 | secure_origin,\n 208 | secure_target_origin,\n 209 | username,\n 210 | username,\n 211 | decrypted_password,\n 212 | username_field,\n 213 | password_field,\n 214 | !exists);","lines":23,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-password-import.c#L192-214"}]},
{"id":"PANIC_11","type":"panic","type_label":"Panic","severity":"medium","difficulty":"easy","description":"CSV import inner loop indexes header_row[j] for every field j present in a data row without checking header_row[j] is non-NULL. parse_csv NUL-terminates each row; a data row with more fields than the header causes reads past the header's NULL terminator (out-of-bounds), passed to g_strcmp0.","fix":"Bound the inner loop by both arrays: `for (j = 0; row[j] && header_row[j]; j++)`.","locations":[{"ref":"lib/sync/ephy-password-import.c:388-393","code":" 388 | for (guint j = 0; row[j]; j++) {\n 389 | const char *field_value;\n 390 | const char *field_name;\n 391 | \n 392 | field_value = row[j];\n 393 | field_name = header_row[j];","lines":6,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-password-import.c#L388-393"}]},
{"id":"PANIC_12","type":"panic","type_label":"Panic","severity":"medium","difficulty":"easy","description":"commit_batch_cb passes the X-Last-Modified response header directly to g_ascii_strtod without a NULL check. soup_message_headers_get_one() returns NULL if the header is absent; g_ascii_strtod(NULL, ...) dereferences NULL and crashes. A malformed/proxied 200 response missing the header triggers this.","fix":"Guard: `if (last_modified) ephy_synchronizable_manager_set_sync_time(...);` else log and skip.","locations":[{"ref":"lib/sync/ephy-sync-service.c:1219-1220","code":" 1219 | last_modified = soup_message_headers_get_one (response_headers, \"X-Last-Modified\");\n 1220 | ephy_synchronizable_manager_set_sync_time (data->manager, g_ascii_strtod (last_modified, NULL));","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-service.c#L1219-1220"}]},
{"id":"PANIC_13","type":"panic","type_label":"Panic","severity":"medium","difficulty":"easy","description":"command_to_node() passes command->shortcut and command->description directly to json_object_set_string_member(), which asserts/crashes on a NULL value. commands.update() sets command->shortcut = NULL for an empty shortcut, and description may be absent in the manifest. Reachable via commands.getAll() after an update that cleared the shortcut.","fix":"Only set each member when non-NULL, or use json_object_set_null_member() for NULL values.","locations":[{"ref":"src/webextension/api/commands.c:141-143","code":" 141 | json_object_set_string_member (obj, \"name\", command->name);\n 142 | json_object_set_string_member (obj, \"shortcut\", command->shortcut);\n 143 | json_object_set_string_member (obj, \"description\", command->description);","lines":3,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/commands.c#L141-143"},{"ref":"src/webextension/api/commands.c:243-249","code":" 243 | shortcut = ephy_json_object_get_string (details, \"shortcut\");\n 244 | if (shortcut && !*shortcut) {\n 245 | /* Empty string is set to nothing. */\n 246 | g_free (command->accelerator);\n 247 | g_free (command->shortcut);\n 248 | command->accelerator = NULL;\n 249 | command->shortcut = NULL;","lines":7,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/commands.c#L243-249"}]},
{"id":"PANIC_14","type":"panic","type_label":"Panic","severity":"medium","difficulty":"easy","description":"tabs_handler_remove() passes tab_ids (result of ephy_json_array_get_element) straight to JSON_NODE_HOLDS_ARRAY() and later to ephy_json_node_get_int() without a NULL check. ephy_json_array_get_element returns NULL when index 0 is out of bounds (tabs.remove() called with no arguments). JSON_NODE_HOLDS_ARRAY(NULL) dereferences NULL.","fix":"Check `if (!tab_ids) { g_task_return_new_error(...); return; }` before using tab_ids.","locations":[{"ref":"src/webextension/api/tabs.c:687-704","code":" 687 | JsonNode *tab_ids = ephy_json_array_get_element (args, 0);\n 688 | gint64 tab_id;\n 689 | \n 690 | /* FIXME: Epiphany should use webkit_web_view_try_close() and this should wait until after that. */\n 691 | \n 692 | if (JSON_NODE_HOLDS_ARRAY (tab_ids)) {\n 693 | JsonArray *array = json_node_get_array (tab_ids);\n 694 | \n 695 | for (guint i = 0; i < json_array_get_length (array); i++) {\n 696 | gint64 tab_id = ephy_json_array_get_int (array, i);\n 697 | if (tab_id != -1)\n 698 | close_tab_id (shell, tab_id);\n 699 | }\n 700 | \n 701 | g_task_return_pointer (task, NULL, NULL);\n 702 | }\n 703 | \n 704 | if ((tab_id = ephy_json_node_get_int (tab_ids)) != -1) {","lines":18,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/tabs.c#L687-704"}]},
{"id":"PANIC_15","type":"panic","type_label":"Panic","severity":"medium","difficulty":"medium","description":"windows.getCurrent/getLastFocused/create can pass a NULL or non-EphyWindow pointer to add_window_to_json(). gtk_application_get_active_window() may return NULL (no active window); EPHY_WINDOW(NULL) plus ephy_window_get_tab_view(NULL) crashes. The getCurrent-from-background path casts gtk_widget_get_root() with EPHY_WINDOW without verifying the type.","fix":"NULL-check and EPHY_IS_WINDOW-check the window before add_window_to_json; return an error if no suitable window exists.","locations":[{"ref":"src/webextension/api/windows.c:173-180","code":" 173 | if (sender->view == ephy_web_extension_manager_get_background_web_view (manager, sender->extension))\n 174 | window = EPHY_WINDOW (gtk_application_get_active_window (GTK_APPLICATION (ephy_shell_get_default ())));\n 175 | else\n 176 | window = EPHY_WINDOW (gtk_widget_get_root (GTK_WIDGET (sender->view)));\n 177 | \n 178 | populate_tabs = get_info ? ephy_json_object_get_boolean (get_info, \"populate\", FALSE) : FALSE;\n 179 | \n 180 | add_window_to_json (sender->extension, builder, window, populate_tabs);","lines":8,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/windows.c#L173-180"},{"ref":"src/webextension/api/windows.c:197-200","code":" 197 | window = EPHY_WINDOW (gtk_application_get_active_window (GTK_APPLICATION (ephy_shell_get_default ())));\n 198 | populate_tabs = get_info ? ephy_json_object_get_boolean (get_info, \"populate\", FALSE) : FALSE;\n 199 | \n 200 | add_window_to_json (sender->extension, builder, window, populate_tabs);","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/windows.c#L197-200"}]},
{"id":"PANIC_16","type":"panic","type_label":"Panic","severity":"medium","difficulty":"easy","description":"web_extension_add_content_script() calls json_object_get_string_member_with_default(object, \"run_at\", ...). The raw json-glib variant returns the default only when the member is ABSENT; if 'run_at' is present but not a string (untrusted manifest), it asserts/crashes (type confusion).","fix":"Use ephy_json_object_get_string(object, \"run_at\") and fall back to \"document_idle\" when NULL.","locations":[{"ref":"src/webextension/ephy-web-extension.c:373","code":" 373 | run_at = json_object_get_string_member_with_default (object, \"run_at\", \"document_idle\");","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension.c#L373"}]},
{"id":"PANIC_17","type":"panic","type_label":"Panic","severity":"medium","difficulty":"easy","description":"generate_background_page() iterates background.scripts with json_array_get_string_element() and passes the result to g_uri_escape_string(). For a non-string array element json_array_get_string_element returns NULL/asserts and g_uri_escape_string(NULL,...) crashes. The array is untrusted manifest data.","fix":"Use ephy_json_array_get_string(scripts, i) and skip entries that are NULL/non-strings.","locations":[{"ref":"src/webextension/ephy-web-extension.c:421-429","code":" 421 | for (unsigned int i = 0; i < json_array_get_length (scripts); i++) {\n 422 | const char *script_file = json_array_get_string_element (scripts, i);\n 423 | g_autofree char *escaped_script_file = g_uri_escape_string (script_file, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, FALSE);\n 424 | \n 425 | g_string_append_printf (background_page,\n 426 | \"<script type=\\\"text/javascript\\\" src=\\\"ephy-webextension://%s/%s\\\"><\/script>\",\n 427 | ephy_web_extension_get_guid (self),\n 428 | escaped_script_file);\n 429 | }","lines":9,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension.c#L421-429"}]},
{"id":"PANIC_18","type":"panic","type_label":"Panic","severity":"medium","difficulty":"easy","description":"ephy_web_extension_rule_matches_uri() logs error->message when match_pattern is NULL, but webkit_web_extension_match_pattern_new_with_string can return NULL without guaranteeing error is set; the `!match_pattern || error` condition can fire with error == NULL, dereferencing NULL. Rules come from untrusted manifest content (content_script matches, host permissions).","fix":"Use `error ? error->message : \"unknown error\"` and only access error when non-NULL.","locations":[{"ref":"src/webextension/ephy-web-extension.c:1464-1468","code":" 1464 | match_pattern = webkit_web_extension_match_pattern_new_with_string (rule, &error);\n 1465 | if (!match_pattern || error) {\n 1466 | g_warning (\"Failed to parse rule '%s': %s\", rule, error->message);\n 1467 | return FALSE;\n 1468 | }","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension.c#L1464-1468"}]},
{"id":"PANIC_19","type":"panic","type_label":"Panic","severity":"low","difficulty":"medium","description":"tls_error_page_message_received_cb parses view->tls_error_failing_uri with g_uri_parse and passes g_uri_get_host(uri) without checking uri for NULL. On parse failure g_uri_get_host(NULL) dereferences NULL.","fix":"Check uri (and the host) for NULL after g_uri_parse and bail out gracefully.","locations":[{"ref":"embed/ephy-web-view.c:1240-1246","code":" 1240 | g_assert (G_IS_TLS_CERTIFICATE (view->certificate));\n 1241 | g_assert (view->tls_error_failing_uri);\n 1242 | \n 1243 | uri = g_uri_parse (view->tls_error_failing_uri, G_URI_FLAGS_PARSE_RELAXED, NULL);\n 1244 | webkit_network_session_allow_tls_certificate_for_host (ephy_embed_shell_get_network_session (shell),\n 1245 | view->certificate,\n 1246 | g_uri_get_host (uri));","lines":7,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-web-view.c#L1240-1246"}]},
{"id":"PANIC_20","type":"panic","type_label":"Panic","severity":"low","difficulty":"medium","description":"find_path_start() asserts g_assert(after_scheme) after strstr(url, \"//\"). The url comes from a downloaded OpenSearch description file's template attribute, which is attacker-controllable. A template URL with no \"//\" sequence aborts the process via the assertion instead of returning a clean parse error.","fix":"Replace the assertion with a graceful error: if (!after_scheme) { set *error and return NULL; }.","locations":[{"ref":"lib/ephy-opensearch-engine.c:93-96","code":" 93 | const char *after_scheme = strstr (url, \"//\");\n 94 | const char *path_start;\n 95 | \n 96 | g_assert (after_scheme);","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/ephy-opensearch-engine.c#L93-96"}]},
{"id":"PANIC_21","type":"panic","type_label":"Panic","severity":"low","difficulty":"medium","description":"context_cmd_send_via_email passes `label` (from webkit_hit_test_result_get_link_label) directly to g_uri_escape_string(). webkit_hit_test_result_get_link_label() can return NULL for links without text content (e.g. an image wrapped in an <a>), and g_uri_escape_string(NULL, ...) dereferences NULL. The non-link branch uses ephy_embed_get_title which is also not guaranteed non-NULL.","fix":"Guard label/address against NULL (substitute \"\" ) before calling g_uri_escape_string().","locations":[{"ref":"src/context-menu-commands.c:179-193","code":" 179 | if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) {\n 180 | label = webkit_hit_test_result_get_link_label (hit_test_result);\n 181 | address = webkit_hit_test_result_get_link_uri (hit_test_result);\n 182 | } else {\n 183 | EphyEmbed *embed;\n 184 | \n 185 | embed = ephy_embed_container_get_active_child (EPHY_EMBED_CONTAINER (window));\n 186 | g_assert (embed);\n 187 | \n 188 | label = ephy_embed_get_title (embed);\n 189 | address = ephy_web_view_get_address (ephy_embed_get_web_view (embed));\n 190 | }\n 191 | \n 192 | subject = g_uri_escape_string (label, NULL, TRUE);\n 193 | body = g_uri_escape_string (address, NULL, TRUE);","lines":15,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/context-menu-commands.c#L179-193"}]},
{"id":"PANIC_22","type":"panic","type_label":"Panic","severity":"low","difficulty":"medium","description":"ephy_tab_view_get_nth_page(), ephy_tab_view_get_page_index() and ephy_tab_view_get_is_pinned() pass the result of adw_tab_view_get_nth_page()/adw_tab_view_get_page() straight into adw_tab_page_get_child()/adw_tab_view_get_page_position()/adw_tab_page_get_pinned() with no NULL check. Any caller passing an out-of-range index or a widget that is not a current page will dereference NULL. ephy_tab_view_get_nth_page is called from several places (session restore, suggestion tabs query) with indices derived from external/persisted state.","fix":"Return NULL / -1 / FALSE when the underlying AdwTabPage lookup returns NULL, mirroring the guard already present in ephy_tab_view_get_selected_index() and ephy_tab_view_select_page().","locations":[{"ref":"src/ephy-tab-view.c:284-291","code":" 284 | GtkWidget *\n 285 | ephy_tab_view_get_nth_page (EphyTabView *self,\n 286 | int index)\n 287 | {\n 288 | AdwTabPage *page = adw_tab_view_get_nth_page (self->tab_view, index);\n 289 | \n 290 | return adw_tab_page_get_child (page);\n 291 | }","lines":8,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-tab-view.c#L284-291"},{"ref":"src/ephy-tab-view.c:275-282","code":" 275 | int\n 276 | ephy_tab_view_get_page_index (EphyTabView *self,\n 277 | GtkWidget *widget)\n 278 | {\n 279 | AdwTabPage *page = adw_tab_view_get_page (self->tab_view, widget);\n 280 | \n 281 | return adw_tab_view_get_page_position (self->tab_view, page);\n 282 | }","lines":8,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-tab-view.c#L275-282"},{"ref":"src/ephy-tab-view.c:349-356","code":" 349 | gboolean\n 350 | ephy_tab_view_get_is_pinned (EphyTabView *self,\n 351 | GtkWidget *widget)\n 352 | {\n 353 | AdwTabPage *page = adw_tab_view_get_page (self->tab_view, widget);\n 354 | \n 355 | return adw_tab_page_get_pinned (page);\n 356 | }","lines":8,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-tab-view.c#L349-356"}]},
{"id":"PANIC_23","type":"panic","type_label":"Panic","severity":"low","difficulty":"easy","description":"on_command_activated() casts ephy_shell_get_active_web_view() with EPHY_WEB_VIEW() at function entry; if there is no active web view the value is NULL and later page-action handling proceeds with a NULL view/button. The view is only needed in the page-action branch.","fix":"Move the view lookup into the page-action branch and NULL-check view/button before use.","locations":[{"ref":"src/webextension/api/commands.c:35-44","code":" 35 | EphyWebView *view = EPHY_WEB_VIEW (ephy_shell_get_active_web_view (shell));\n 36 | GtkWidget *button;\n 37 | const char *command_name = g_object_get_data (G_OBJECT (action), \"command-name-json\");\n 38 | \n 39 | if (strcmp (command_name, \"\\\"_execute_browser_action\\\"\") == 0) {\n 40 | ephy_web_extension_manager_show_browser_action (manager, self);\n 41 | return;\n 42 | } else if (strcmp (command_name, \"\\\"_execute_page_action\\\"\") == 0) {\n 43 | button = ephy_web_extension_manager_get_page_action (manager, self, view);\n 44 | gtk_widget_mnemonic_activate (button, false);","lines":10,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/commands.c#L35-44"}]},
{"id":"PANIC_24","type":"panic","type_label":"Panic","severity":"low","difficulty":"medium","description":"window_cmd_navigation_new_tab() calls webkit_back_forward_list_get_back_item()/get_forward_item() and immediately passes the result to webkit_back_forward_list_item_get_original_uri() without a NULL check. If the list has no back/forward item the item is NULL and original_uri dereferences NULL. Although the corresponding actions are usually disabled when navigation is unavailable, this is reachable via accelerators/middle-click on stale state.","fix":"NULL-check back_item/forward_item before calling get_original_uri and bail out (or open a blank tab) when NULL.","locations":[{"ref":"src/window-commands.c:1316-1352","code":" 1316 | if (strstr (g_action_get_name (G_ACTION (action)), \"back\")) {\n 1317 | const char *back_uri;\n 1318 | WebKitBackForwardList *history;\n 1319 | WebKitBackForwardListItem *back_item;\n 1320 | \n 1321 | history = webkit_web_view_get_back_forward_list (web_view);\n 1322 | back_item = webkit_back_forward_list_get_back_item (history);\n 1323 | back_uri = webkit_back_forward_list_item_get_original_uri (back_item);\n 1324 | \n 1325 | embed = ephy_shell_new_tab (ephy_shell_get_default (),\n 1326 | EPHY_WINDOW (gtk_widget_get_root (GTK_WIDGET (embed))),\n 1327 | NULL,\n 1328 | 0);\n 1329 | \n 1330 | web_view = EPHY_GET_WEBKIT_WEB_VIEW_FROM_EMBED (embed);\n 1331 | webkit_web_view_load_uri (web_view, back_uri);\n 1332 | gtk_widget_grab_focus (GTK_WIDGET (embed));\n 1333 | } else {\n 1334 | const char *forward_uri;\n 1335 | WebKitBackForwardList *history;\n 1336 | WebKitBackForwardListItem *forward_item;\n 1337 | \n 1338 | /* Forward history is not copied when opening\n 1339 | * a new tab, so get the forward URI manually\n 1340 | * and load it */\n 1341 | history = webkit_web_view_get_back_forward_list (EPHY_GET_WEBKIT_WEB_VIEW_FROM_EMBED (embed));\n 1342 | forward_item = webkit_back_forward_list_get_forward_item (history);\n 1343 | forward_uri = webkit_back_forward_list_item_get_original_uri (forward_item);\n 1344 | \n 1345 | embed = ephy_shell_new_tab (ephy_shell_get_default (),\n 1346 | EPHY_WINDOW (gtk_widget_get_root (GTK_WIDGET (embed))),\n 1347 | embed,\n 1348 | 0);\n 1349 | \n 1350 | web_view = EPHY_GET_WEBKIT_WEB_VIEW_FROM_EMBED (embed);\n 1351 | webkit_web_view_load_uri (web_view, forward_uri);\n 1352 | }","lines":37,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/window-commands.c#L1316-1352"}]},
{"id":"UAF_1","type":"use_after_free","type_label":"Use After Free","severity":"critical","difficulty":"easy","description":"ephy_browser_action_finalize() frees badge_text (a char* from g_strdup_printf) and badge_color (a GdkRGBA* from gdk_rgba_copy) with g_clear_object(). g_clear_object calls g_object_unref on non-GObject memory, reading a bogus refcount and corrupting the heap / crashing. The setters correctly use g_free and gdk_rgba_free, confirming the types.","fix":"Use g_clear_pointer(&self->badge_text, g_free) and g_clear_pointer(&self->badge_color, gdk_rgba_free).","locations":[{"ref":"src/webextension/ephy-browser-action.c:91-92","code":" 91 | g_clear_object (&self->badge_text);\n 92 | g_clear_object (&self->badge_color);","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-browser-action.c#L91-92"}]},
{"id":"UAF_2","type":"use_after_free","type_label":"Use After Free","severity":"high","difficulty":"medium","description":"readability_js_finish_cb early-returns on cancelled/error WITHOUT removing the EphyReaderRequest from outstanding_requests and without freeing it. The request leaks and stays in outstanding_requests with an already-cancelled cancellable. (view-source's equivalent handles the error path correctly via finish_uri_scheme_request.)","fix":"On the error/cancel early-return, call finish_uri_scheme_request with an error (which removes from the list and frees) instead of a bare return.","locations":[{"ref":"embed/ephy-reader-handler.c:171-176","code":" 171 | value = webkit_web_view_evaluate_javascript_finish (web_view, result, &error);\n 172 | if (!value) {\n 173 | if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))\n 174 | g_warning (\"Error running javascript: %s\", error->message);\n 175 | return;\n 176 | }","lines":6,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-reader-handler.c#L171-176"}]},
{"id":"UAF_3","type":"use_after_free","type_label":"Use After Free","severity":"high","difficulty":"medium","description":"EphySendMessageData stores guid as a non-owning const char*. In ephy_webextension_install_common_apis it is set to the caller's string, which (for default/private world callers) points into a GUri (g_uri_get_host) freed when the window-object-cleared callback returns, or into a WebKitScriptWorld name. The ephy_send_message JS function lives as long as the JS context and later dereferences send_message_data->guid, reading freed/dangling memory.","fix":"Make the struct own the guid (g_strdup) and free it in a destroy func passed to jsc_value_new_function instead of plain g_free.","locations":[{"ref":"embed/web-process-extension/ephy-webextension-common.c:25-29","code":" 25 | typedef struct {\n 26 | WebKitWebPage *page;\n 27 | WebKitFrame *frame;\n 28 | const char *guid;\n 29 | } EphySendMessageData;","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-webextension-common.c#L25-29"},{"ref":"embed/web-process-extension/ephy-webextension-common.c:251-267","code":" 251 | /* global functions */\n 252 | send_message_data = g_new (EphySendMessageData, 1);\n 253 | send_message_data->page = page;\n 254 | send_message_data->frame = frame;\n 255 | send_message_data->guid = guid;\n 256 | js_function = jsc_value_new_function (js_context,\n 257 | NULL,\n 258 | G_CALLBACK (ephy_send_message),\n 259 | send_message_data, g_free,\n 260 | G_TYPE_NONE,\n 261 | 4,\n 262 | G_TYPE_STRING,\n 263 | JSC_TYPE_VALUE,\n 264 | JSC_TYPE_VALUE,\n 265 | JSC_TYPE_VALUE);\n 266 | jsc_context_set_value (js_context, \"ephy_send_message\", js_function);\n 267 | g_clear_object (&js_function);","lines":17,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-webextension-common.c#L251-267"},{"ref":"embed/web-process-extension/ephy-web-process-extension.c:1011-1043","code":" 1011 | guid = g_uri_get_host (parsed_uri);\n 1012 | extension_data = g_hash_table_lookup (extension->web_extensions, guid);\n 1013 | if (!extension_data) {\n 1014 | g_warning (\"Failed to find extension by guid: %s\", guid);\n 1015 | return;\n 1016 | }\n 1017 | \n 1018 | js_context = webkit_frame_get_js_context_for_script_world (frame, world);\n 1019 | \n 1020 | /* The first context made is assumed to be the background page. */\n 1021 | if (!extension->background_context && extension_data->has_background_page)\n 1022 | extension->background_context = js_context;\n 1023 | \n 1024 | if (!g_hash_table_contains (extension->view_contexts, GUINT_TO_POINTER (webkit_frame_get_id (frame)))) {\n 1025 | g_hash_table_insert (extension->view_contexts, GUINT_TO_POINTER (webkit_frame_get_id (frame)), g_object_ref (js_context));\n 1026 | g_object_weak_ref (G_OBJECT (frame), on_frame_destroyed, extension);\n 1027 | }\n 1028 | \n 1029 | js_browser = jsc_context_get_value (js_context, \"browser\");\n 1030 | g_assert (!jsc_value_is_object (js_browser));\n 1031 | \n 1032 | bytes = g_resources_lookup_data (\"/org/gnome/epiphany-web-process-extension/js/webextensions-common.js\", G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);\n 1033 | data = g_bytes_get_data (bytes, &data_size);\n 1034 | result = jsc_context_evaluate_with_source_uri (js_context, data, data_size, \"resource:///org/gnome/epiphany-web-process-extension/js/webextensions-common.js\", 1);\n 1035 | g_clear_pointer (&bytes, g_bytes_unref);\n 1036 | g_clear_object (&result);\n 1037 | \n 1038 | ephy_webextension_install_common_apis (page,\n 1039 | frame,\n 1040 | js_context,\n 1041 | guid,\n 1042 | extension_data->translations,\n 1043 | extension_data->manifest);","lines":33,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-web-process-extension.c#L1011-1043"}]},
{"id":"UAF_4","type":"use_after_free","type_label":"Use After Free","severity":"high","difficulty":"medium","description":"EphyFullscreenBox schedules a one-shot hide timeout (start_hide_timeout -> g_timeout_add_once with `self` as data) but the dispose handler never cancels it. If the widget is disposed while a hide timeout is pending, hide_timeout_cb fires on freed memory. Every other timeout user in this slice (action-bar-end, header-bar, downloads-paintable, site-menu-button) clears its handle id in dispose; this one does not.","fix":"Add `g_clear_handle_id (&self->timeout_id, g_source_remove);` to ephy_fullscreen_box_dispose().","locations":[{"ref":"src/ephy-fullscreen-box.c:308-321","code":" 308 | static void\n 309 | ephy_fullscreen_box_dispose (GObject *object)\n 310 | {\n 311 | EphyFullscreenBox *self = EPHY_FULLSCREEN_BOX (object);\n 312 | \n 313 | if (self->toolbar_view) {\n 314 | gtk_widget_unparent (GTK_WIDGET (self->toolbar_view));\n 315 | self->toolbar_view = NULL;\n 316 | }\n 317 | \n 318 | g_clear_pointer (&self->headers, g_list_free);\n 319 | \n 320 | G_OBJECT_CLASS (ephy_fullscreen_box_parent_class)->dispose (object);\n 321 | }","lines":14,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-fullscreen-box.c#L308-321"},{"ref":"src/ephy-fullscreen-box.c:88-100","code":" 88 | static void\n 89 | start_hide_timeout (EphyFullscreenBox *self)\n 90 | {\n 91 | if (!adw_toolbar_view_get_reveal_top_bars (self->toolbar_view))\n 92 | return;\n 93 | \n 94 | if (self->timeout_id)\n 95 | return;\n 96 | \n 97 | self->timeout_id = g_timeout_add_once (FULLSCREEN_HIDE_DELAY,\n 98 | (GSourceOnceFunc)hide_timeout_cb,\n 99 | self);\n 100 | }","lines":13,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-fullscreen-box.c#L88-100"}]},
{"id":"UAF_5","type":"use_after_free","type_label":"Use After Free","severity":"high","difficulty":"medium","description":"EphyLocationEntry schedules a high-priority idle (calc_and_set_prefix) storing 'self' as user_data with no extra ref and no GDestroyNotify, and ephy_location_entry_dispose() never removes self->idle_id. If the entry is disposed/finalized while the completion idle is still pending, calc_and_set_prefix() will run on freed memory (EPHY_LOCATION_ENTRY (user_data) on a dead object). The dns_prefetch_handle_id timeout is also not cleared in dispose (it is mitigated only because the PrefetchHelper holds a ref on self, but the idle is not).","fix":"In ephy_location_entry_dispose(), add g_clear_handle_id (&self->idle_id, g_source_remove); (and g_clear_handle_id for dns_prefetch_handle_id). Note idle_id must be a guint for g_clear_handle_id (see separate finding).","locations":[{"ref":"src/ephy-location-entry.c:771-773","code":" 771 | if (self->insert_completion)\n 772 | self->idle_id = g_idle_add_full (G_PRIORITY_HIGH, calc_and_set_prefix, self, NULL);\n 773 | }","lines":3,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-location-entry.c#L771-773"},{"ref":"src/ephy-location-entry.c:999-1014","code":" 999 | ephy_location_entry_dispose (GObject *object)\n 1000 | {\n 1001 | EphyLocationEntry *self = EPHY_LOCATION_ENTRY (object);\n 1002 | \n 1003 | g_clear_pointer (&self->suggestions_popover, gtk_widget_unparent);\n 1004 | \n 1005 | if (self->text) {\n 1006 | gtk_editable_finish_delegate (GTK_EDITABLE (self));\n 1007 | self->text = NULL;\n 1008 | }\n 1009 | \n 1010 | g_clear_handle_id (&self->progress_timeout, g_source_remove);\n 1011 | ephy_location_entry_page_action_clear (self);\n 1012 | \n 1013 | G_OBJECT_CLASS (ephy_location_entry_parent_class)->dispose (object);\n 1014 | }","lines":16,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-location-entry.c#L999-1014"}]},
{"id":"UAF_6","type":"use_after_free","type_label":"Use After Free","severity":"high","difficulty":"medium","description":"In tab_has_modified_forms_cb(), when the tab has no modified forms but is not the selected page, the code does `data->window->previous_embed = FALSE;`. previous_embed is an EphyEmbed* tracked via a weak pointer (set/cleared elsewhere with g_set_weak_pointer/g_clear_weak_pointer). Directly overwriting it with FALSE (NULL) bypasses g_clear_weak_pointer, leaving the GObject's registered weak-pointer location dangling; when that embed is later finalized GLib writes through the stale address. The sibling function tab_has_modified_forms_dialog_cb() correctly sets `go_to_previous_tab = FALSE` here instead - this is a copy-paste error.","fix":"Replace `data->window->previous_embed = FALSE;` with `data->window->go_to_previous_tab = FALSE;` (matching tab_has_modified_forms_dialog_cb), or use g_clear_weak_pointer (&data->window->previous_embed) if the pointer really must be cleared.","locations":[{"ref":"src/ephy-window.c:3572-3574","code":" 3572 | if (!has_modified_forms) {\n 3573 | if (ephy_tab_view_get_selected_page (data->window->tab_view) != GTK_WIDGET (data->embed))\n 3574 | data->window->previous_embed = FALSE;","lines":3,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-window.c#L3572-3574"}]},
{"id":"UAF_7","type":"use_after_free","type_label":"Use After Free","severity":"high","difficulty":"medium","description":"EphyBrowserActionRow connects notify::badge-text and notify::badge-color on self->browser_action with plain g_signal_connect and never disconnects them in dispose, even though browser_action is owned externally and can outlive the row. The dangling handlers then fire on a finalized row (use-after-free).","fix":"Use g_signal_connect_object(..., self, 0), or store and g_signal_handler_disconnect the handler ids in dispose before clearing browser_action.","locations":[{"ref":"src/webextension/ephy-browser-action-row.c:114-115","code":" 114 | g_signal_connect (EPHY_BROWSER_ACTION (self->browser_action), \"notify::badge-text\", G_CALLBACK (on_badge_text), self);\n 115 | g_signal_connect (EPHY_BROWSER_ACTION (self->browser_action), \"notify::badge-color\", G_CALLBACK (on_badge_color), self);","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-browser-action-row.c#L114-115"},{"ref":"src/webextension/ephy-browser-action-row.c:121-128","code":" 121 | ephy_browser_action_row_dispose (GObject *object)\n 122 | {\n 123 | EphyBrowserActionRow *self = EPHY_BROWSER_ACTION_ROW (object);\n 124 | \n 125 | g_clear_object (&self->browser_action);\n 126 | \n 127 | G_OBJECT_CLASS (ephy_browser_action_row_parent_class)->dispose (object);\n 128 | }","lines":8,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-browser-action-row.c#L121-128"}]},
{"id":"UAF_8","type":"use_after_free","type_label":"Use After Free","severity":"high","difficulty":"easy","description":"ephy_web_extension_get_custom_css() advances its loop with `list = list->data` instead of `list = list->next`. list->data is the WebExtensionCustomCSS* payload, not a GList node; treating it as a GList* and dereferencing ->data/->next is a type-confused pointer walk that reads arbitrary memory (crash / UAF) and breaks the duplicate-CSS lookup.","fix":"Change the iterator to `list = list->next`.","locations":[{"ref":"src/webextension/ephy-web-extension.c:1376","code":" 1376 | for (GList *list = self->custom_css; list && list->data; list = list->data) {","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension.c#L1376"}]},
{"id":"UAF_9","type":"use_after_free","type_label":"Use After Free","severity":"medium","difficulty":"medium","description":"ephy-prefs-dialog uses inconsistent ownership for self->extensions_page: it is set with a plain assignment from g_object_new (sync_extensions), but cleared with g_clear_weak_pointer. The field was never registered with g_set_weak_pointer, so g_clear_weak_pointer just NULLs it without ever having installed a weak ref; if the page is destroyed by its container the field is left dangling (no auto-null), and the asymmetry vs sync_features (plain assign + = NULL) is a latent dangling-pointer/UAF risk.","fix":"Make it consistent: either register with g_set_weak_pointer when created and keep g_clear_weak_pointer, or use a plain field cleared with self->extensions_page = NULL (matching sync_features).","locations":[{"ref":"src/preferences/ephy-prefs-dialog.c:150-159","code":" 150 | if (enable_extensions && !self->extensions_page) {\n 151 | self->extensions_page = g_object_new (EPHY_TYPE_PREFS_EXTENSIONS_PAGE, NULL);\n 152 | g_signal_connect (self->extensions_page, \"extension-row-activated\", G_CALLBACK (on_extension_row_activated), self);\n 153 | adw_preferences_dialog_add (ADW_PREFERENCES_DIALOG (self),\n 154 | ADW_PREFERENCES_PAGE (self->extensions_page));\n 155 | } else if (self->extensions_page) {\n 156 | adw_preferences_dialog_remove (ADW_PREFERENCES_DIALOG (self),\n 157 | ADW_PREFERENCES_PAGE (self->extensions_page));\n 158 | g_clear_weak_pointer (&self->extensions_page);\n 159 | }","lines":10,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/preferences/ephy-prefs-dialog.c#L150-159"},{"ref":"src/preferences/ephy-prefs-dialog.c:172-176","code":" 172 | } else if (self->features_page) {\n 173 | adw_preferences_dialog_remove (ADW_PREFERENCES_DIALOG (self),\n 174 | ADW_PREFERENCES_PAGE (self->features_page));\n 175 | self->features_page = NULL;\n 176 | }","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/preferences/ephy-prefs-dialog.c#L172-176"}]},
{"id":"UAF_10","type":"use_after_free","type_label":"Use After Free","severity":"medium","difficulty":"medium","description":"application_window_added_cb() heap-allocates WindowAddedCallbackData storing a raw EphyWebExtension* and schedules a 1-second timeout; the timeout (application_window_added_timeout_cb) dereferences data->web_extension. If the extension is deactivated/uninstalled (and freed) within that second, the timeout uses a dangling pointer. The window is re-validated by id but the extension pointer is not.","fix":"Hold a weak reference (g_object_add_weak_pointer) or a full ref on the extension in the callback data and verify/clear it in the timeout; or cancel the timeout when the extension is deactivated.","locations":[{"ref":"src/webextension/ephy-web-extension-manager.c:1587-1599","code":" 1587 | static void\n 1588 | application_window_added_cb (EphyShell *shell,\n 1589 | EphyWindow *window,\n 1590 | EphyWebExtension *web_extension)\n 1591 | {\n 1592 | /* At this point the EphyWindow has no EphyEmbed child so we can't get information from it.\n 1593 | * We also can't really know ahead of time if multiple tabs are opening so simply adding a delay\n 1594 | * tends to give accurate results. */\n 1595 | WindowAddedCallbackData *data = g_new (WindowAddedCallbackData, 1);\n 1596 | data->window_id = ephy_window_get_uid (window);\n 1597 | data->web_extension = web_extension;\n 1598 | g_timeout_add_seconds_full (G_PRIORITY_DEFAULT_IDLE, 1, application_window_added_timeout_cb, data, g_free);\n 1599 | }","lines":13,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension-manager.c#L1587-1599"},{"ref":"src/webextension/ephy-web-extension-manager.c:1554-1577","code":" 1554 | static gboolean\n 1555 | application_window_added_timeout_cb (gpointer user_data)\n 1556 | {\n 1557 | WindowAddedCallbackData *data = user_data;\n 1558 | EphyWebExtensionManager *manager = ephy_web_extension_manager_get_default ();\n 1559 | EphyWindow *window = ephy_web_extension_api_windows_get_window_for_id (data->window_id);\n 1560 | EphyTabView *tab_view;\n 1561 | AdwTabView *adw_tab_view;\n 1562 | g_autofree char *window_json = NULL;\n 1563 | \n 1564 | /* It is possible the window was removed before this timeout. */\n 1565 | if (!window)\n 1566 | return G_SOURCE_REMOVE;\n 1567 | \n 1568 | window_json = ephy_web_extension_api_windows_create_window_json (data->web_extension, window);\n 1569 | ephy_web_extension_manager_emit_in_extension_views (manager, data->web_extension, \"windows.onCreated\", window_json);\n 1570 | \n 1571 | tab_view = ephy_window_get_tab_view (window);\n 1572 | adw_tab_view = ephy_tab_view_get_tab_view (tab_view);\n 1573 | g_signal_connect (G_OBJECT (adw_tab_view), \"page-attached\", G_CALLBACK (on_page_attached), data->web_extension);\n 1574 | g_signal_connect (G_OBJECT (adw_tab_view), \"page-detached\", G_CALLBACK (on_page_detached), data->web_extension);\n 1575 | \n 1576 | return G_SOURCE_REMOVE;\n 1577 | }","lines":24,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension-manager.c#L1554-1577"}]},
{"id":"MEM_1","type":"memory_leak","type_label":"Memory Leak","severity":"high","difficulty":"medium","description":"ephy_bookmark_tags_compare leaks two heap strings on EVERY call: g_utf8_casefold(tag1,-1) and g_utf8_casefold(tag2,-1) are passed to g_strcmp0 and never freed. This function is the GCompareDataFunc used for all tag GSequence inserts/searches/lookups across the bookmarks subsystem, so it runs constantly (every add_tag, create_tag, has_tag, sort). Cumulative leak proportional to tag operations.","fix":"Use g_autofree temporaries: g_autofree char *cf1 = g_utf8_casefold(tag1,-1); g_autofree char *cf2 = g_utf8_casefold(tag2,-1); result_casefolded = g_strcmp0(cf1, cf2);","locations":[{"ref":"src/bookmarks/ephy-bookmark.c:611-612","code":" 611 | result_casefolded = g_strcmp0 (g_utf8_casefold (tag1, -1),\n 612 | g_utf8_casefold (tag2, -1));","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmark.c#L611-612"},{"ref":"src/bookmarks/ephy-bookmark.c:622-623","code":" 622 | if (result_casefolded == 0)\n 623 | return result;","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmark.c#L622-623"}]},
{"id":"MEM_2","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"medium","description":"gck_slot_get_token_info() returns a transfer-full GckTokenInfo. It is fetched in object_details_cb (uri_data.token_info) and in session_opened_cb (info) and used to build strings, but never freed - a GckTokenInfo leaks per certificate inspected and per PIN dialog shown.","fix":"Wrap in g_autoptr(GckTokenInfo) or call gck_token_info_free after use.","locations":[{"ref":"embed/ephy-client-certificate-manager.c:179-180","code":" 179 | uri_data.token_info = gck_slot_get_token_info (gck_session_get_slot (self->session));\n 180 | uri = gck_uri_data_build (&uri_data, GCK_URI_FOR_OBJECT_ON_TOKEN);","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-client-certificate-manager.c#L179-180"},{"ref":"embed/ephy-client-certificate-manager.c:300-316","code":" 300 | GckTokenInfo *info;\n 301 | \n 302 | self->session = gck_slot_open_session_finish (slot, res, &error);\n 303 | if (error) {\n 304 | if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))\n 305 | g_warning (\"Could not open client certificate session: %s\", error->message);\n 306 | cancel_authentication (self);\n 307 | return;\n 308 | }\n 309 | \n 310 | dialog = adw_alert_dialog_new (_(\"PIN required\"), NULL);\n 311 | \n 312 | info = gck_slot_get_token_info (slot);\n 313 | body = g_strdup_printf (_(\"Please enter PIN for %s, to authenticate at %s:%d.\"),\n 314 | info->label,\n 315 | webkit_authentication_request_get_host (self->request),\n 316 | webkit_authentication_request_get_port (self->request));","lines":17,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-client-certificate-manager.c#L300-316"}]},
{"id":"MEM_3","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"medium","description":"process_next_object pops each GckObject from self->objects with g_list_remove WITHOUT unreffing it; self->objects came from gck_enumerator_next_finish as a transfer-full list of refs. Each processed object's ref is leaked (only the remaining unprocessed list is unreffed on free).","fix":"g_object_unref the popped object once the async lookup that consumes it completes.","locations":[{"ref":"embed/ephy-client-certificate-manager.c:204-223","code":" 204 | static void\n 205 | process_next_object (EphyClientCertificateManager *self)\n 206 | {\n 207 | GckObject *object;\n 208 | const gulong attr_types[] = {\n 209 | CKA_ID, CKA_LABEL, CKA_ISSUER,\n 210 | CKA_VALUE, CKA_CLASS\n 211 | };\n 212 | \n 213 | if (!self->objects) {\n 214 | cancel_authentication (self);\n 215 | gck_session_logout_async (self->session, self->cancellable, on_session_logout, self);\n 216 | return;\n 217 | }\n 218 | \n 219 | object = self->objects->data;\n 220 | self->objects = g_list_remove (self->objects, object);\n 221 | \n 222 | gck_object_get_async (object, attr_types, sizeof (attr_types) / sizeof (attr_types[0]), self->cancellable, object_details_cb, self);\n 223 | }","lines":20,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-client-certificate-manager.c#L204-223"}]},
{"id":"MEM_4","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"medium","description":"download_response_changed_cb overwrites download->content_type with g_content_type_from_mime_type() without freeing the previous value. notify::response can fire more than once, leaking the old content_type string.","fix":"g_clear_pointer (&download->content_type, g_free) before assigning.","locations":[{"ref":"embed/ephy-download.c:871-887","code":" 871 | static void\n 872 | download_response_changed_cb (WebKitDownload *wk_download,\n 873 | GParamSpec *spec,\n 874 | EphyDownload *download)\n 875 | {\n 876 | WebKitURIResponse *response;\n 877 | const char *mime_type;\n 878 | \n 879 | response = webkit_download_get_response (download->download);\n 880 | mime_type = webkit_uri_response_get_mime_type (response);\n 881 | if (!mime_type)\n 882 | return;\n 883 | \n 884 | download->content_type = g_content_type_from_mime_type (mime_type);\n 885 | if (download->content_type)\n 886 | g_object_notify_by_pspec (G_OBJECT (download), obj_properties[PROP_CONTENT_TYPE]);\n 887 | }","lines":17,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-download.c#L871-887"}]},
{"id":"MEM_5","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"easy","description":"web_application_delete_response_cb frees the EphyWebApplicationDeleteData only when ephy_web_application_delete() succeeds; on failure (FALSE) data and data->app_id leak. The dialog is one-shot with no other cleanup path.","fix":"Free data unconditionally after handling the response (only reload on success).","locations":[{"ref":"embed/ephy-web-view.c:1285-1292","code":" 1285 | static void\n 1286 | web_application_delete_response_cb (EphyWebApplicationDeleteData *data)\n 1287 | {\n 1288 | if (ephy_web_application_delete (data->app_id, NULL)) {\n 1289 | webkit_web_view_reload (data->view);\n 1290 | ephy_web_application_delete_data_free (data);\n 1291 | }\n 1292 | }","lines":8,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-web-view.c#L1285-1292"}]},
{"id":"MEM_6","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"easy","description":"has_modified_forms_cb, when the 2-second timeout already fired (id == 0), jumps to `out` and unrefs the task without consuming `error`. webkit_web_view_evaluate_javascript_finish typically returns a GError (e.g. cancelled) here, leaked on every slow JS evaluation / process hang.","fix":"g_clear_error (&error) before goto out.","locations":[{"ref":"embed/ephy-web-view.c:3279-3296","code":" 3279 | value = webkit_web_view_evaluate_javascript_finish (view, result, &error);\n 3280 | \n 3281 | id = GPOINTER_TO_INT (g_task_get_task_data (task));\n 3282 | if (id == 0) {\n 3283 | /* We hit the timeout. Our task has already returned. */\n 3284 | goto out;\n 3285 | }\n 3286 | g_source_remove (id);\n 3287 | \n 3288 | if (!value) {\n 3289 | g_task_return_error (task, error);\n 3290 | } else {\n 3291 | retval = jsc_value_to_boolean (value);\n 3292 | g_task_return_boolean (task, retval);\n 3293 | }\n 3294 | \n 3295 | out:\n 3296 | g_object_unref (task);","lines":18,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-web-view.c#L3279-3296"}]},
{"id":"MEM_7","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"medium","description":"ephy_autofill_js_get_field_value passes jsc_weak_value_new(cb) as user_data; web_view_get_field_value_ready_cb retrieves the value but never unrefs the JSCWeakValue on any path (success or the cancelled/error/!cb/!parameters early-returns). Each getFieldValue call leaks one JSCWeakValue.","fix":"Adopt user_data as g_autoptr(JSCWeakValue) at the top of the callback so it is freed on every return path.","locations":[{"ref":"embed/web-process-extension/ephy-web-process-extension.c:1070-1121","code":" 1070 | static void\n 1071 | web_view_get_field_value_ready_cb (WebKitWebPage *web_page,\n 1072 | GAsyncResult *result,\n 1073 | gpointer user_data)\n 1074 | {\n 1075 | WebKitUserMessage *reply = NULL;\n 1076 | g_autoptr (JSCValue) cb = jsc_weak_value_get_value (user_data);\n 1077 | g_autoptr (GVariant) parameters = NULL;\n 1078 | g_autoptr (GError) error = NULL;\n 1079 | g_autoptr (JSCValue) ret = NULL;\n 1080 | const char *value;\n 1081 | \n 1082 | reply = webkit_web_page_send_message_to_view_finish (web_page, result, &error);\n 1083 | if (error) {\n 1084 | if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))\n 1085 | g_warning (\"Error getting autofill field value from WebView: %s\", error->message);\n 1086 | return;\n 1087 | }\n 1088 | \n 1089 | if (!cb)\n 1090 | return;\n 1091 | \n 1092 | parameters = webkit_user_message_get_parameters (reply);\n 1093 | if (!parameters)\n 1094 | return;\n 1095 | \n 1096 | g_variant_get (parameters, \"(&s)\", &value);\n 1097 | \n 1098 | ret = jsc_value_function_call (cb, G_TYPE_STRING, value, G_TYPE_NONE);\n 1099 | (void)ret;\n 1100 | }\n 1101 | \n 1102 | static void\n 1103 | ephy_autofill_js_get_field_value (EphyAutofillField field,\n 1104 | guint64 page_id,\n 1105 | JSCValue *cb,\n 1106 | EphyWebProcessExtension *extension)\n 1107 | {\n 1108 | WebKitWebPage *web_page;\n 1109 | WebKitUserMessage *message;\n 1110 | \n 1111 | web_page = webkit_web_process_extension_get_page (extension->extension, page_id);\n 1112 | if (!web_page)\n 1113 | return;\n 1114 | \n 1115 | message = webkit_user_message_new (\"EphyAutoFill.GetFieldValue\",\n 1116 | g_variant_new (\"(t)\", field));\n 1117 | webkit_web_page_send_message_to_view (web_page, message,\n 1118 | extension->cancellable,\n 1119 | (GAsyncReadyCallback)web_view_get_field_value_ready_cb,\n 1120 | jsc_weak_value_new (cb));\n 1121 | }","lines":52,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-web-process-extension.c#L1070-1121"}]},
{"id":"MEM_8","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"easy","description":"ephy_search_engine_finalize() frees name, url and bang but not suggestions_url or opensearch_url, both of which are heap-allocated strdup'd in their setters. Every EphySearchEngine with a suggestions/opensearch URL leaks those strings on destruction.","fix":"Add g_clear_pointer (&self->suggestions_url, g_free); and g_clear_pointer (&self->opensearch_url, g_free); to the finalizer.","locations":[{"ref":"lib/ephy-search-engine.c:164-174","code":" 164 | static void\n 165 | ephy_search_engine_finalize (GObject *object)\n 166 | {\n 167 | EphySearchEngine *self = (EphySearchEngine *)object;\n 168 | \n 169 | g_clear_pointer (&self->name, g_free);\n 170 | g_clear_pointer (&self->url, g_free);\n 171 | g_clear_pointer (&self->bang, g_free);\n 172 | \n 173 | G_OBJECT_CLASS (ephy_search_engine_parent_class)->finalize (object);\n 174 | }","lines":11,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/ephy-search-engine.c#L164-174"}]},
{"id":"MEM_9","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"easy","description":"add_to_bookmarks_order_table builds key = g_strconcat(type, ':', item, NULL) and passes it to gvdb_hash_table_insert_variant. gvdb_hash_table_insert (third-party/gvdb/gvdb-builder.c:106-109) g_strdup's the key, so the g_strconcat result is never freed -> one leaked string per bookmarks-order entry on every .gvdb export (and export runs on essentially every bookmark mutation).","fix":"Use g_autofree char *key = g_strconcat (type, \":\", item, NULL); (or g_free(key) after the insert).","locations":[{"ref":"src/bookmarks/ephy-bookmarks-export.c:117-127","code":" 117 | const char *type, *item, *key;\n 118 | int index;\n 119 | GVariant *saved_variant;\n 120 | \n 121 | g_variant_get (variant, \"(ssi)\", &type, &item, &index);\n 122 | key = g_strconcat (type, \":\", item, NULL);\n 123 | \n 124 | /* A duplicate of the variant is stored so Epiphany can still access the\n 125 | * original one while running. */\n 126 | saved_variant = g_variant_new (\"(ssi)\", type, item, index);\n 127 | gvdb_hash_table_insert_variant (table, key, saved_variant);","lines":11,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmarks-export.c#L117-127"}]},
{"id":"MEM_10","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"medium","description":"In ephy_bookmarks_import, the GVariant returned by gvdb_table_get_value for each entry of the 'tags-order' and 'bookmarks-order' tables is never unref'd. For tags-order the variant is handed to ephy_bookmarks_manager_tags_order_add_tag_variant (which stores it, so that one is consumed), but for bookmarks-order the variant from gvdb_table_get_value at line 175 is read with g_variant_get and then dropped without g_variant_unref.","fix":"g_variant_unref (variant) after use in the bookmarks-order loop. (For tags-order confirm ownership transfer; if not transferred, unref there too.)","locations":[{"ref":"src/bookmarks/ephy-bookmarks-import.c:176-183","code":" 176 | GVariant *variant = gvdb_table_get_value (table, list[i]);\n 177 | const char *type, *item;\n 178 | int index;\n 179 | \n 180 | g_variant_get (variant, \"(ssi)\", &type, &item, &index);\n 181 | \n 182 | ephy_bookmarks_manager_add_to_bookmarks_order (manager, type, item, index);\n 183 | }","lines":8,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmarks-import.c#L176-183"},{"ref":"src/bookmarks/ephy-bookmarks-import.c:146-151","code":" 146 | GVariant *variant = gvdb_table_get_value (table, list[i]);\n 147 | const char *variant_tag;\n 148 | \n 149 | g_variant_get (variant, \"(sa(si))\", &variant_tag, NULL);\n 150 | \n 151 | ephy_bookmarks_manager_tags_order_add_tag_variant (manager, variant);","lines":6,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmarks-import.c#L146-151"}]},
{"id":"MEM_11","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"easy","description":"EphyBookmarksManager finalize frees only self->bookmarks and self->tags. self->bookmarks_order and self->tags_order GSequences (allocated in init) are never freed -> leaked for the lifetime of the manager (one per process, but a clear leak).","fix":"Add g_clear_pointer (&self->bookmarks_order, g_sequence_free); g_clear_pointer (&self->tags_order, g_sequence_free); in ephy_bookmarks_manager_finalize.","locations":[{"ref":"src/bookmarks/ephy-bookmarks-manager.c:120-130","code":" 120 | static void\n 121 | ephy_bookmarks_manager_finalize (GObject *object)\n 122 | {\n 123 | EphyBookmarksManager *self = EPHY_BOOKMARKS_MANAGER (object);\n 124 | \n 125 | g_sequence_free (self->bookmarks);\n 126 | g_sequence_free (self->tags);\n 127 | g_free (self->gvdb_filename);\n 128 | \n 129 | G_OBJECT_CLASS (ephy_bookmarks_manager_parent_class)->finalize (object);\n 130 | }","lines":11,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmarks-manager.c#L120-130"},{"ref":"src/bookmarks/ephy-bookmarks-manager.c:263-264","code":" 263 | self->bookmarks_order = g_sequence_new (g_free);\n 264 | self->tags_order = g_sequence_new (g_free);","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmarks-manager.c#L263-264"}]},
{"id":"MEM_12","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"medium","description":"load_auto_open_schemes_table() obtains a GVariantIter via g_settings_get(..., \"a(ss)\", &iter) but never frees it. g_settings_get with an array format allocates the iterator which must be freed with g_variant_iter_free(). Leaks on every call (called from url_should_open_automatically and add_auto_open_scheme, i.e. on external-scheme navigations).","fix":"After the while loop, call g_variant_iter_free (iter); (or use g_autoptr (GVariantIter)).","locations":[{"ref":"src/ephy-window.c:2299-2314","code":" 2299 | static GHashTable *\n 2300 | load_auto_open_schemes_table (void)\n 2301 | {\n 2302 | g_autoptr (GHashTable) table = NULL;\n 2303 | GVariantIter *iter = NULL;\n 2304 | char *pref_uri;\n 2305 | char *pref_scheme;\n 2306 | \n 2307 | table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);\n 2308 | g_settings_get (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_AUTO_OPEN_SCHEMES, \"a(ss)\", &iter);\n 2309 | \n 2310 | while (g_variant_iter_next (iter, \"(ss)\", &pref_uri, &pref_scheme))\n 2311 | g_hash_table_insert (table, pref_uri, pref_scheme);\n 2312 | \n 2313 | return g_steal_pointer (&table);\n 2314 | }","lines":16,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-window.c#L2299-2314"}]},
{"id":"MEM_13","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"medium","description":"add_auto_open_scheme() calls load_auto_open_schemes_table() into a local `GHashTable *table` and never frees it. The returned hash table (and its keys, which are g_free destroy-noted) is leaked on every invocation.","fix":"Add g_hash_table_unref (table); before returning (or declare `g_autoptr (GHashTable) table`).","locations":[{"ref":"src/ephy-window.c:2383-2404","code":" 2383 | static void\n 2384 | add_auto_open_scheme (char *openeer_uri,\n 2385 | char *target_scheme)\n 2386 | {\n 2387 | GVariantBuilder builder;\n 2388 | GVariant *variant;\n 2389 | GHashTable *table = load_auto_open_schemes_table ();\n 2390 | GHashTableIter iter;\n 2391 | gpointer key;\n 2392 | gpointer value;\n 2393 | \n 2394 | g_variant_builder_init (&builder, G_VARIANT_TYPE (\"a(ss)\"));\n 2395 | g_hash_table_iter_init (&iter, table);\n 2396 | \n 2397 | while (g_hash_table_iter_next (&iter, &key, &value))\n 2398 | g_variant_builder_add (&builder, \"(ss)\", key, value);\n 2399 | \n 2400 | g_variant_builder_add (&builder, \"(ss)\", openeer_uri, target_scheme);\n 2401 | \n 2402 | variant = g_variant_builder_end (&builder);\n 2403 | g_settings_set_value (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_AUTO_OPEN_SCHEMES, variant);\n 2404 | }","lines":22,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-window.c#L2383-2404"}]},
{"id":"MEM_14","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"easy","description":"on_bind_list_item takes text = g_strsplit_set(lang_string, \":\", -1)[0]; it keeps element [0] (owned string) but never frees the returned GStrv array, leaking the array structure plus every other split element on every list-item bind (called repeatedly while scrolling the add-language list).","fix":"g_auto (GStrv) parts = g_strsplit_set (lang_string, \":\", -1); gtk_label_set_text (GTK_LABEL (label), parts[0]); (let g_auto free the array).","locations":[{"ref":"src/preferences/prefs-general-page.c:324-326","code":" 324 | text = g_strsplit_set (lang_string, \":\", -1)[0];\n 325 | \n 326 | gtk_label_set_text (GTK_LABEL (label), text);","lines":3,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/preferences/prefs-general-page.c#L324-326"}]},
{"id":"MEM_15","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"easy","description":"In on_model_updated() each suggestion fetched with g_list_model_get_item() is a transfer-full reference (per the GListModel contract), but only its URI is g_strdup()'d into the results array - the EphySuggestion reference itself is never unref'd. Every search query through the GNOME Shell search provider leaks one EphySuggestion object per result, indefinitely for the lifetime of the service process.","fix":"Wrap the item in g_autoptr (EphySuggestion) suggestion (or call g_object_unref (suggestion) at the end of each loop iteration) so the reference returned by g_list_model_get_item() is released.","locations":[{"ref":"src/search-provider/ephy-search-provider.c:77-80","code":" 77 | for (guint i = 0; i < n_items; i++) {\n 78 | suggestion = g_list_model_get_item (G_LIST_MODEL (self->model), i);\n 79 | g_ptr_array_add (results, g_strdup (ephy_suggestion_get_uri (suggestion)));\n 80 | }","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/search-provider/ephy-search-provider.c#L77-80"}]},
{"id":"MEM_16","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"easy","description":"document_uri and target_uri are allocated with g_uri_parse() but never freed, leaking a GUri (sometimes two) on every context-menu build for an extension that has menus.","fix":"Declare both as g_autoptr(GUri), or g_uri_unref() them before returning.","locations":[{"ref":"src/webextension/api/menus.c:691-708","code":" 691 | GUri *document_uri;\n 692 | GUri *target_uri = NULL;\n 693 | \n 694 | if (!menus)\n 695 | return NULL;\n 696 | \n 697 | parse_context_menu_user_data (context_menu, &selected_text, &is_editable, &is_password);\n 698 | tab_data = create_tabdata (self, web_view);\n 699 | action = g_action_map_lookup_action (G_ACTION_MAP (ephy_shell_get_default ()), \"webextension-context-menu\");\n 700 | g_assert (action);\n 701 | \n 702 | document_uri = g_uri_parse (webkit_web_view_get_uri (web_view), G_URI_FLAGS_PARSE_RELAXED | G_URI_FLAGS_ENCODED_PATH | G_URI_FLAGS_ENCODED_QUERY | G_URI_FLAGS_SCHEME_NORMALIZE, NULL);\n 703 | if (webkit_hit_test_result_get_link_uri (hit_test_result))\n 704 | target_uri = g_uri_parse (webkit_hit_test_result_get_link_uri (hit_test_result), G_URI_FLAGS_PARSE_RELAXED | G_URI_FLAGS_ENCODED_PATH | G_URI_FLAGS_ENCODED_QUERY | G_URI_FLAGS_SCHEME_NORMALIZE, NULL);\n 705 | \n 706 | return create_context_menu_item (menus, ephy_web_extension_get_short_name (self), self, web_view, modifiers,\n 707 | context_menu, hit_test_result, action, is_audio, is_video, is_editable,\n 708 | is_password, selected_text, tab_data, document_uri, target_uri);","lines":18,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/menus.c#L691-708"}]},
{"id":"MEM_17","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"easy","description":"pageAction.setIcon path uses g_object_get(action, \"child\", &child, NULL), which returns a NEW reference to the child widget that is never unreffed (leak per call). Also 'child' is used as GTK_IMAGE(child) without a NULL check.","fix":"Use a transfer-none accessor (e.g. gtk_button_get_child) instead of g_object_get, or g_object_unref(child) after use; add a NULL check before GTK_IMAGE().","locations":[{"ref":"src/webextension/api/pageaction.c:110-112","code":" 110 | g_object_get (action, \"child\", &child, NULL);\n 111 | \n 112 | gtk_image_set_from_paintable (GTK_IMAGE (child), GDK_PAINTABLE (texture));","lines":3,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/pageaction.c#L110-112"}]},
{"id":"MEM_18","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"medium","description":"set_badge_color() creates a fresh GtkCssProvider via gtk_css_provider_new() on every call and adds it to the style context, but never unrefs it and never removes previously added providers. Repeated badge-color updates leak a provider each time and accumulate stale providers.","fix":"Store a single reusable provider on the instance (update + unref in dispose), or g_object_unref after add_provider and remove the previous provider first.","locations":[{"ref":"src/webextension/ephy-indicator-bin.c:435-460","code":" 435 | void\n 436 | ephy_indicator_bin_set_badge_color (EphyIndicatorBin *self,\n 437 | GdkRGBA *color)\n 438 | {\n 439 | GtkCssProvider *provider;\n 440 | GtkStyleContext *context;\n 441 | g_autofree char *css = NULL;\n 442 | \n 443 | gtk_widget_remove_css_class (GTK_WIDGET (self), \"needs-attention\");\n 444 | \n 445 | G_GNUC_BEGIN_IGNORE_DEPRECATIONS\n 446 | if (color) {\n 447 | g_autofree char *col_str = gdk_rgba_to_string (color);\n 448 | css = g_strdup_printf (\".needs-attention > indicator { background-color: %s; }\", col_str);\n 449 | provider = gtk_css_provider_new ();\n 450 | gtk_css_provider_load_from_data (provider, css, -1);\n 451 | context = gtk_widget_get_style_context (GTK_WIDGET (self->indicator));\n 452 | gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);\n 453 | \n 454 | gtk_widget_add_css_class (GTK_WIDGET (self), \"needs-attention\");\n 455 | }\n 456 | G_GNUC_END_IGNORE_DEPRECATIONS\n 457 | \n 458 | gtk_widget_queue_draw (GTK_WIDGET (self));\n 459 | g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BADGE]);\n 460 | }","lines":26,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-indicator-bin.c#L435-460"}]},
{"id":"MEM_19","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"easy","description":"decompress_xpi_thread() does `const char *filename = g_file_get_path (file);`. g_file_get_path returns a transfer-full allocated string assigned to a const char* that is never freed - leaked on every XPI installation.","fix":"Use g_autofree char *filename = g_file_get_path(file);","locations":[{"ref":"src/webextension/ephy-web-extension-manager.c:538","code":" 538 | const char *filename = g_file_get_path (file);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension-manager.c#L538"}]},
{"id":"MEM_20","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"easy","description":"webkit_pref_callback_user_stylesheet logs g_file_get_uri() in g_warning on monitor-creation failure; the allocated URI string is never freed.","fix":"Store the URI in a g_autofree char * (and use g_autoptr(GError) for consistency).","locations":[{"ref":"embed/ephy-embed-prefs.c:150-153","code":" 150 | user_style_sheet_monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE, NULL, &error);\n 151 | if (!user_style_sheet_monitor) {\n 152 | g_warning (\"Could not create a file monitor for %s: %s\", g_file_get_uri (file), error->message);\n 153 | g_error_free (error);","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-embed-prefs.c#L150-153"}]},
{"id":"MEM_21","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"easy","description":"ephy_embed_utils_link_message_parse passes g_strdup_printf() result directly to g_string_new(), which copies the string but does not take ownership, leaking the g_strdup_printf buffer on every mailto: link hover.","fix":"Use g_string_new(NULL) + g_string_append_printf(), or free the temporary.","locations":[{"ref":"embed/ephy-embed-utils.c:74-75","code":" 74 | tmp = g_string_new (g_strdup_printf (_(\"Send an email message to “%s”\"),\n 75 | (splitted_message[0] + 7)));","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-embed-utils.c#L74-75"}]},
{"id":"MEM_22","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"medium","description":"gnome_parse_locale() lazily compiles a static GRegex but, in the error path where g_regex_match() reports a partial/non match, it only warns and goes to 'out' without ever freeing match_info created earlier... (note: match_info IS freed at out). The genuine leak: when g_regex_new() succeeds the regex is intentionally cached, fine; however collect_locales_from_directory() calls scandir and frees only the top-level 'dirents' array via free(dirents) but never frees the individual dirents[cnt] entries that scandir() allocated with malloc, leaking one struct dirent per directory entry on every locale collection.","fix":"Free each dirents[cnt] (free(dirents[cnt])) inside the loop before free(dirents), as required by scandir()'s contract.","locations":[{"ref":"lib/contrib/gnome-languages.c:498-508","code":" 498 | ndirents = scandir (LIBLOCALEDIR, &dirents, select_dirs, alphasort);\n 499 | \n 500 | for (cnt = 0; cnt < ndirents; ++cnt) {\n 501 | if (add_locale (dirents[cnt]->d_name, TRUE))\n 502 | found_locales = TRUE;\n 503 | }\n 504 | \n 505 | if (ndirents > 0) {\n 506 | free (dirents);\n 507 | }\n 508 | return found_locales;","lines":11,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/contrib/gnome-languages.c#L498-508"}]},
{"id":"MEM_23","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"medium","description":"ephy_copy_directory() passes two freshly g_build_filename()-allocated strings directly into the recursive call, which only consumes them via g_file_new_for_path() (which copies). The recursive callee never frees its 'source'/'target' arguments, so both allocations leak on every recursed child entry.","fix":"Capture the results in g_autofree char* locals (or free after the recursive call) instead of passing inline g_build_filename() results.","locations":[{"ref":"lib/ephy-file-helpers.c:851-854","code":" 851 | ephy_copy_directory (\n 852 | g_build_filename (source, g_file_info_get_name (info), NULL),\n 853 | g_build_filename (target, g_file_info_get_name (info), NULL));\n 854 | }","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/ephy-file-helpers.c#L851-854"}]},
{"id":"MEM_24","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"medium","description":"ephy_sync_crypto_decrypt_record and ephy_synchronizable_from_bso decode the AES key, HMAC key and decrypted cleartext (sensitive key material / plaintext credentials) but free them with plain g_free without zeroing first. Sensitive material is left in freed heap memory. The crypto keys derived in derive_master_keys / derive_session_token etc. are likewise g_free'd without wiping.","fix":"Wipe sensitive buffers before freeing (e.g. explicit_bzero / gnutls_memset / memset_s, or g_clear_pointer with a zeroing free helper).","locations":[{"ref":"lib/sync/ephy-sync-crypto.c:1159-1162","code":" 1159 | g_free (ciphertext);\n 1160 | g_free (iv);\n 1161 | g_free (aes_key);\n 1162 | g_free (hmac_key);","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-crypto.c#L1159-1162"},{"ref":"lib/sync/ephy-sync-crypto.c:602-611","code":" 602 | *token_id = g_malloc (len);\n 603 | *req_hmac_key = g_malloc (len);\n 604 | *request_key = g_malloc (len);\n 605 | memcpy (*token_id, out, len);\n 606 | memcpy (*req_hmac_key, out + len, len);\n 607 | memcpy (*request_key, out + 2 * len, len);\n 608 | \n 609 | g_free (token);\n 610 | g_free (out);\n 611 | g_free (info);","lines":10,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-crypto.c#L602-611"}]},
{"id":"MEM_25","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"easy","description":"In send_and_read_async_ready_cb and storage_request_async_ready_cb, when soup_session_send_and_read_finish fails it logs error->message but never frees the GError. The error is leaked on every failed request.","fix":"Call g_error_free(error) (or use g_autoptr(GError)) in the failure branch of each callback.","locations":[{"ref":"lib/sync/ephy-sync-service.c:198-214","code":" 198 | GBytes *bytes;\n 199 | SoupMessage *msg;\n 200 | GError *error = NULL;\n 201 | \n 202 | bytes = soup_session_send_and_read_finish (session, result, &error);\n 203 | if (!bytes) {\n 204 | g_warning (\"Failed to send request: %s\", error->message);\n 205 | g_error_free (error);\n 206 | }\n 207 | \n 208 | msg = soup_session_get_async_result_message (session, result);\n 209 | g_object_set_data_full (G_OBJECT (msg),\n 210 | \"ephy-request-body\", bytes ? bytes : g_bytes_new (NULL, 0),\n 211 | (GDestroyNotify)g_bytes_unref);\n 212 | data->callback (session, msg, data->user_data);\n 213 | g_free (data);\n 214 | }","lines":17,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-service.c#L198-214"},{"ref":"lib/sync/ephy-sync-service.c:220-237","code":" 220 | {\n 221 | GBytes *bytes;\n 222 | SoupMessage *msg;\n 223 | GError *error = NULL;\n 224 | \n 225 | bytes = soup_session_send_and_read_finish (session, result, &error);\n 226 | if (!bytes) {\n 227 | g_warning (\"Failed to send storage request: %s\", error->message);\n 228 | g_error_free (error);\n 229 | }\n 230 | \n 231 | msg = soup_session_get_async_result_message (session, result);\n 232 | g_object_set_data_full (G_OBJECT (msg),\n 233 | \"ephy-request-body\", bytes ? bytes : g_bytes_new (NULL, 0),\n 234 | (GDestroyNotify)g_bytes_unref);\n 235 | data->callback (session, msg, data->user_data);\n 236 | storage_request_async_data_free (data);\n 237 | }","lines":18,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-service.c#L220-237"}]},
{"id":"MEM_26","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"medium","description":"ephy_bookmarks_dialog_init creates filter and filter_model (a GtkFilterListModel wrapping a ref of the manager) but the filter_model is g_autoptr-scoped and never attached to any view or stored. It is constructed and immediately destroyed at end of init - dead code that also takes and drops an extra ref on the manager. The string filter bound to search_entry is likewise unused.","fix":"Remove the unused filter/filter_model construction, or wire filter_model into the list box that is supposed to use it.","locations":[{"ref":"src/bookmarks/ephy-bookmarks-dialog.c:1346-1355","code":" 1346 | GtkFilter *filter;\n 1347 | g_autoptr (GtkFilterListModel) filter_model = NULL;\n 1348 | \n 1349 | gtk_widget_init_template (GTK_WIDGET (self));\n 1350 | \n 1351 | self->manager = ephy_shell_get_bookmarks_manager (ephy_shell_get_default ());\n 1352 | \n 1353 | filter = GTK_FILTER (gtk_string_filter_new (gtk_property_expression_new (EPHY_TYPE_BOOKMARK, NULL, \"title\")));\n 1354 | g_object_bind_property (self->search_entry, \"text\", filter, \"search\", 0);\n 1355 | filter_model = gtk_filter_list_model_new (G_LIST_MODEL (g_object_ref (self->manager)), filter);","lines":10,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmarks-dialog.c#L1346-1355"}]},
{"id":"MEM_27","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"medium","description":"save_property_url() allocates SavePropertyURLData and connects it to the download's \"filename-suggested\" signal, but it is only freed inside filename_confirmed_cb, which is reached only if the dialog flow runs. If the download fails/finishes before filename-suggested is emitted (or the signal is otherwise never emitted), the SavePropertyURLData (title string, window ref, download ref) leaks and the download ref is never released.","fix":"Own the data with a GDestroyNotify tied to the download object lifetime (e.g. g_signal_connect_data with a destroy callback, or attach via g_object_set_data_full) so it is freed when the download is finalized even if the signal never fires.","locations":[{"ref":"src/context-menu-commands.c:273-295","code":" 273 | static void\n 274 | save_property_url (const char *title,\n 275 | EphyWindow *window,\n 276 | const char *property)\n 277 | {\n 278 | WebKitHitTestResult *hit_test_result;\n 279 | g_autofree char *location = NULL;\n 280 | EphyDownload *download;\n 281 | SavePropertyURLData *data;\n 282 | \n 283 | hit_test_result = ephy_window_get_context_event (window);\n 284 | g_assert (hit_test_result);\n 285 | \n 286 | g_object_get (hit_test_result, property, &location, NULL);\n 287 | download = ephy_download_new_for_uri (location);\n 288 | data = g_new (SavePropertyURLData, 1);\n 289 | data->title = g_strdup (title);\n 290 | data->window = g_object_ref (window);\n 291 | data->download = download;\n 292 | g_signal_connect (download, \"filename-suggested\",\n 293 | G_CALLBACK (filename_suggested_cb),\n 294 | data);\n 295 | }","lines":23,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/context-menu-commands.c#L273-295"}]},
{"id":"MEM_28","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"medium","description":"create_row() in ephy-history-dialog.c builds 'date' via ephy_time_helpers_utf_friendly_time(...) and passes the returned string directly to gtk_label_new(). ephy_time_helpers_utf_friendly_time returns a newly allocated char* (the same function is g_free'd at its other call site in ephy-firefox-sync-dialog.c sync_set_last_sync_time). gtk_label_new copies the text, so the returned string is leaked here.","fix":"Store the result in a g_autofree char* (or temporary) and free it after gtk_label_new().","locations":[{"ref":"src/ephy-history-dialog.c:536","code":" 536 | date = gtk_label_new (ephy_time_helpers_utf_friendly_time (url->last_visit_time / 1000000));","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-history-dialog.c#L536"}]},
{"id":"MEM_29","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"easy","description":"on_add_lang_dialog_response calls g_strsplit_set twice on the same string and indexes [0] and [1], freeing desc and code individually but leaking both GStrv arrays (and any extra elements) for each selected language. It also parses twice unnecessarily.","fix":"g_auto (GStrv) parts = g_strsplit_set (lang_string, \":\", -1); desc = parts[0]; code = parts[1]; language_editor_add (...); and drop the manual g_free of desc/code (do not free substrings owned by the array).","locations":[{"ref":"src/preferences/prefs-general-page.c:358-364","code":" 358 | desc = g_strsplit_set (lang_string, \":\", -1)[0];\n 359 | code = g_strsplit_set (lang_string, \":\", -1)[1];\n 360 | \n 361 | language_editor_add (general_page, code, desc);\n 362 | \n 363 | g_free (desc);\n 364 | g_free (code);","lines":7,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/preferences/prefs-general-page.c#L358-364"}]},
{"id":"MEM_30","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"easy","description":"In prefs-search-engine-page on_button_row_activated the delete branch sets self->engine = NULL without unreffing, but self->engine was created via g_value_dup_object (owns a ref) and finalize only g_clear_objects it. The plain NULL assignment leaks that GObject reference.","fix":"Use g_clear_object (&self->engine); instead of self->engine = NULL;","locations":[{"ref":"src/preferences/prefs-search-engine-page.c:253-255","code":" 253 | ephy_search_engine_manager_delete_engine (self->manager, self->engine);\n 254 | self->engine = NULL;\n 255 | } else {","lines":3,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/preferences/prefs-search-engine-page.c#L253-255"}]},
{"id":"MEM_31","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"easy","description":"ephy_browser_action_set_property() does g_set_object(&self->web_extension, g_value_dup_object(value)). g_value_dup_object returns a +1 ref and g_set_object adds its own ref, so the EphyWebExtension is over-referenced by one (a ref leak; only one unref happens in finalize).","fix":"Use g_set_object(&self->web_extension, g_value_get_object(value)); (let g_set_object take the ref).","locations":[{"ref":"src/webextension/ephy-browser-action.c:129","code":" 129 | g_set_object (&self->web_extension, g_value_dup_object (value));","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-browser-action.c#L129"}]},
{"id":"MEM_32","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"easy","description":"on_extension_decompressed() declares `GFileInfo *file_info` (not g_autoptr) and assigns the transfer-full result of g_file_query_info, but never unrefs it; it is passed to ephy_web_extension_load_async (which refs as needed) and then leaked.","fix":"Declare file_info as g_autoptr(GFileInfo).","locations":[{"ref":"src/webextension/ephy-web-extension-manager.c:484","code":" 484 | GFileInfo *file_info;","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension-manager.c#L484"}]},
{"id":"MEM_33","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"medium","description":"web_extension_parse_commands() calls json_object_get_members(object), which returns a newly-allocated GList that must be g_list_free()d; the list is iterated but never freed (members are owned by the object), leaking one GList per parsed 'commands' object.","fix":"Store the list and g_list_free() it after the loop.","locations":[{"ref":"src/webextension/ephy-web-extension.c:841","code":" 841 | for (GList *list = json_object_get_members (object); list; list = list->next) {","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension.c#L841"}]},
{"id":"MEM_34","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"easy","description":"ephy_web_extension_rule_matches_uri() builds the URL with g_uri_to_string(uri) inline as an argument; the returned allocated string is never freed, leaking on every match check (called per host-permission per URI).","fix":"Store it in `g_autofree char *url = g_uri_to_string(uri);` and pass url.","locations":[{"ref":"src/webextension/ephy-web-extension.c:1470","code":" 1470 | return webkit_web_extension_match_pattern_matches_url (match_pattern, g_uri_to_string (uri), WEBKIT_WEB_EXTENSION_MATCH_PATTERN_OPTIONS_NONE);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension.c#L1470"}]},
{"id":"MEM_35","type":"memory_leak","type_label":"Memory Leak","severity":"negligible","difficulty":"medium","description":"ephy_history_service_get_url_row() returns NULL when ephy_sqlite_statement_step() fails but never frees the GError it passed in (error is set on failure). Minor leak on the error path; most other functions in this file g_error_free their error.","fix":"g_clear_error (&error) before returning NULL (or log it like the surrounding code does).","locations":[{"ref":"lib/history/ephy-history-service-urls-table.c:95-97","code":" 95 | if (!ephy_sqlite_statement_step (statement, &error)) {\n 96 | return NULL;\n 97 | }","lines":3,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/history/ephy-history-service-urls-table.c#L95-97"}]},
{"id":"OVF_1","type":"overflow_underflow","type_label":"Overflow Underflow","severity":"critical","difficulty":"easy","description":"Heap buffer overflow / missing NULL terminator when appending the --search term to the argv array. Operator precedence makes `current_len + 1 * sizeof (char *)` evaluate as `current_len + sizeof(char *)` (i.e. current_len + 8 bytes), NOT `(current_len + 2) * sizeof(char *)`. The array needs room for the existing args, the new search term, AND a trailing NULL. With no other arguments (current_len == 0) only 8 bytes (one pointer) are allocated, then arguments[0] is written and the array is left non-NULL-terminated. The array is subsequently passed to ephy_shell_startup_context_new() and g_strfreev(), both of which read past the end of the allocation.","fix":"Allocate room for the new element plus a NULL terminator and terminate the array, e.g. `arguments = g_realloc (arguments, (current_len + 2) * sizeof (char *)); arguments[current_len] = ephy_embed_utils_autosearch_address (search_term); arguments[current_len + 1] = NULL;`","locations":[{"ref":"src/ephy-main.c:438-443","code":" 438 | if (search_term) {\n 439 | int current_len = arguments ? g_strv_length (arguments) : 0;\n 440 | arguments = g_realloc (arguments, current_len + 1 * sizeof (char *));\n 441 | arguments[current_len] = ephy_embed_utils_autosearch_address (search_term);\n 442 | g_clear_pointer (&search_term, g_free);\n 443 | }","lines":6,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-main.c#L438-443"}]},
{"id":"OVF_2","type":"overflow_underflow","type_label":"Overflow Underflow","severity":"high","difficulty":"easy","description":"In the Chrome/Chromium password importer, the encrypted blob is unconditionally treated as having a 3-byte version prefix: memcmp(password,\"v11\",3) and strncmp(password,\"v10\",3) read 3 bytes without checking that password_size >= 3 (out-of-bounds read on a short blob), and decrypt() is called with (password + 3, password_size - 3). When password_size < 3 the length becomes negative, then in decrypt() g_malloc0(password_length + 1) is invoked with a negative int promoted to a huge size_t and CBC_DECRYPT is given a negative length. The blob contents come from an external SQLite database, so this is a data-controlled crash / huge allocation / OOB read.","fix":"Before the prefix checks add `if (password_size < 3) continue;`, and only call decrypt() when a known v10/v11 prefix actually matched.","locations":[{"ref":"lib/sync/ephy-password-import.c:179-191","code":" 179 | if (memcmp (password, \"v11\", 3) == 0) {\n 180 | /* V11: System based password manager, we only support libsecret */\n 181 | secret_phrase = get_libsecret_phrase (type);\n 182 | } else if (strncmp (password, \"v10\", 3) == 0) {\n 183 | /* V10: Browser based master key: peanuts */\n 184 | secret_phrase = g_strdup (\"peanuts\");\n 185 | }\n 186 | \n 187 | if (!secret_phrase)\n 188 | continue;\n 189 | }\n 190 | \n 191 | decrypted_password = decrypt ((unsigned char *)(password) + 3, password_size - 3, secret_phrase);","lines":13,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-password-import.c#L179-191"},{"ref":"lib/sync/ephy-password-import.c:82-97","code":" 82 | decrypt (unsigned char *password,\n 83 | int password_length,\n 84 | char *phrase)\n 85 | {\n 86 | struct CBC_CTX (struct aes128_ctx, AES_BLOCK_SIZE) aes;\n 87 | unsigned char iv[16] = {0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20};\n 88 | unsigned char salt[9] = {'s', 'a', 'l', 't', 'y', 's', 'a', 'l', 't'};\n 89 | unsigned char key[16];\n 90 | char *out;\n 91 | \n 92 | pbkdf2_hmac_sha1 (strlen (phrase), (unsigned char *)phrase, 1, sizeof (salt), salt, sizeof (key), key);\n 93 | \n 94 | out = g_malloc0 (password_length + 1);\n 95 | aes128_set_decrypt_key (&aes.ctx, key);\n 96 | CBC_SET_IV (&aes, iv);\n 97 | CBC_DECRYPT (&aes, aes128_decrypt, password_length, (unsigned char *)out, password);","lines":16,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-password-import.c#L82-97"}]},
{"id":"OVF_3","type":"overflow_underflow","type_label":"Overflow Underflow","severity":"high","difficulty":"medium","description":"load_xpi_thread() takes archive_entry_size() (attacker-controlled int64 from the XPI header) and passes it to g_malloc0(size) with no bounds/negative check; a negative value sign-extends to a huge gsize (abort), and a large declared size allows a tiny archive to demand a multi-GB allocation (decompression bomb). archive_read_data() returns ssize_t (negative on error) but is stored into a gsize total_len, so an error becomes a huge length, passing the `total_len > 0` check and producing a GBytes whose length exceeds the actual buffer (later OOB reads).","fix":"Validate size (`if (size <= 0 || size > MAX) continue;`), use a signed temporary for archive_read_data and bail on <= 0 before assigning total_len.","locations":[{"ref":"src/webextension/ephy-web-extension.c:1189-1202","code":" 1189 | while (archive_read_next_header (pkg, &entry) == ARCHIVE_OK) {\n 1190 | int64_t size = archive_entry_size (entry);\n 1191 | gsize total_len = 0;\n 1192 | g_autofree char *data = NULL;\n 1193 | \n 1194 | data = g_malloc0 (size);\n 1195 | total_len = archive_read_data (pkg, data, size);\n 1196 | \n 1197 | if (total_len > 0) {\n 1198 | g_hash_table_insert (resources,\n 1199 | g_strdup (archive_entry_pathname (entry)),\n 1200 | g_bytes_new_take (g_steal_pointer (&data), total_len));\n 1201 | }\n 1202 | }","lines":14,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension.c#L1189-1202"}]},
{"id":"OVF_4","type":"overflow_underflow","type_label":"Overflow Underflow","severity":"low","difficulty":"medium","description":"dzl_fuzzy_mutable_index_item_compare() computes 'fa->id - fb->id' (and similarly pos) on 32-bit unsigned bitfield ids and returns the difference as a gint used by g_array_sort. For large id values this subtraction overflows/wraps, producing an inconsistent ordering that violates the comparator contract and can corrupt the sort. Practically the ids stay small, but it's a latent correctness bug.","fix":"Compare with explicit (a<b?-1:a>b?1:0) rather than subtraction.","locations":[{"ref":"lib/contrib/dzl-fuzzy-mutable-index.c:87-100","code":" 87 | static gint\n 88 | dzl_fuzzy_mutable_index_item_compare (gconstpointer a,\n 89 | gconstpointer b)\n 90 | {\n 91 | gint ret;\n 92 | \n 93 | const DzlFuzzyMutableIndexItem *fa = a;\n 94 | const DzlFuzzyMutableIndexItem *fb = b;\n 95 | \n 96 | if ((ret = fa->id - fb->id) == 0)\n 97 | ret = fa->pos - fb->pos;\n 98 | \n 99 | return ret;\n 100 | }","lines":14,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/contrib/dzl-fuzzy-mutable-index.c#L87-100"}]},
{"id":"OVF_5","type":"overflow_underflow","type_label":"Overflow Underflow","severity":"low","difficulty":"medium","description":"ephy_string_shorten() computes the byte length of the shortened UTF-8 prefix as 'GPOINTER_TO_UINT (g_utf8_offset_to_pointer (str, target_length - 1) - str)' into a gulong 'bytes'. The pointer difference is wrapped through GPOINTER_TO_UINT, truncating to unsigned int on LP64; for very long strings (> 4 GiB prefix) this truncates. More importantly the value type juggling (ptrdiff_t -> guint -> gulong) is fragile. Low practical impact given typical UI string sizes.","fix":"Use a ptrdiff_t/gsize directly: 'gsize bytes = g_utf8_offset_to_pointer (str, target_length - 1) - str;' without GPOINTER_TO_UINT.","locations":[{"ref":"lib/ephy-string.c:106-111","code":" 106 | bytes = GPOINTER_TO_UINT (g_utf8_offset_to_pointer (str, target_length - 1) - str);\n 107 | \n 108 | new_str = g_new (gchar, bytes + strlen (\"…\") + 1);\n 109 | \n 110 | strncpy (new_str, str, bytes);\n 111 | strncpy (new_str + bytes, \"…\", strlen (\"…\") + 1);","lines":6,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/ephy-string.c#L106-111"}]},
{"id":"OVF_6","type":"overflow_underflow","type_label":"Overflow Underflow","severity":"low","difficulty":"medium","description":"ephy_sync_utils_decode_hex() allocates strlen(hex)/2 bytes but loops 'for (i=0,j=0; i<strlen(hex); i+=2,j++)'. For an odd-length hex string the final iteration writes decoded[j] at index strlen/2, one byte past the allocation (heap buffer overflow), while reading a partial 1-char hex group. Only an odd-length input triggers it.","fix":"Reject or g_return_val_if_fail on odd-length input, or allocate (strlen(hex)+1)/2 and stop the loop at strlen(hex)-1; also strlen() is recomputed every iteration (perf).","locations":[{"ref":"lib/ephy-sync-utils.c:84-88","code":" 84 | decoded = g_malloc (strlen (hex) / 2);\n 85 | for (gsize i = 0, j = 0; i < strlen (hex); i += 2, j++)\n 86 | sscanf (hex + i, \"%2hhx\", decoded + j);\n 87 | \n 88 | return decoded;","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/ephy-sync-utils.c#L84-88"}]},
{"id":"OVF_7","type":"overflow_underflow","type_label":"Overflow Underflow","severity":"low","difficulty":"easy","description":"on_country_selected / on_card_selected read pos = adw_combo_row_get_selected(...) into an int and index country_map[pos]/card_map[pos] without checking for GTK_INVALID_LIST_POSITION (G_MAXUINT). If no row is selected (reachable during model changes/teardown) pos is a huge value and the array index is wildly out of bounds.","fix":"guint pos = adw_combo_row_get_selected(...); if (pos == GTK_INVALID_LIST_POSITION) return; before indexing.","locations":[{"ref":"src/preferences/autofill-view.c:505-523","code":" 505 | static void\n 506 | on_country_selected (GObject *gobject,\n 507 | GParamSpec *pspec,\n 508 | gpointer user_data)\n 509 | {\n 510 | int pos = adw_combo_row_get_selected (ADW_COMBO_ROW (gobject));\n 511 | ephy_autofill_storage_set (EPHY_AUTOFILL_FIELD_COUNTRY, country_map[pos].name, NULL, NULL, NULL);\n 512 | ephy_autofill_storage_set (EPHY_AUTOFILL_FIELD_COUNTRY_CODE, country_map[pos].code, NULL, NULL, NULL);\n 513 | }\n 514 | \n 515 | static void\n 516 | on_card_selected (GObject *gobject,\n 517 | GParamSpec *pspec,\n 518 | gpointer user_data)\n 519 | {\n 520 | int pos = adw_combo_row_get_selected (ADW_COMBO_ROW (gobject));\n 521 | ephy_autofill_storage_set (EPHY_AUTOFILL_FIELD_CARD_TYPE_NAME, card_map[pos].name, NULL, NULL, NULL);\n 522 | ephy_autofill_storage_set (EPHY_AUTOFILL_FIELD_CARD_TYPE_CODE, card_map[pos].code, NULL, NULL, NULL);\n 523 | }","lines":19,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/preferences/autofill-view.c#L505-523"}]},
{"id":"OVF_8","type":"overflow_underflow","type_label":"Overflow Underflow","severity":"low","difficulty":"easy","description":"timestamp_to_ms() computes (guint)(timestamp - now_ms) and minutes_to_ms() computes (guint)(minutes * 60000) from untrusted double inputs ('when'/'delayInMinutes'/'periodInMinutes'). Large values overflow/truncate when cast to guint, producing a near-arbitrary millisecond delay passed to g_timeout_add_once.","fix":"Validate/clamp the computed delay (reject negatives, cap to a sane maximum such as G_MAXINT) before casting to guint.","locations":[{"ref":"src/webextension/api/alarms.c:69-84","code":" 69 | static guint\n 70 | timestamp_to_ms (double timestamp)\n 71 | {\n 72 | guint64 now_ms = time_now_ms ();\n 73 | \n 74 | if (now_ms > timestamp)\n 75 | return 0;\n 76 | \n 77 | return (guint)(timestamp - now_ms);\n 78 | }\n 79 | \n 80 | static guint\n 81 | minutes_to_ms (double minutes)\n 82 | {\n 83 | return (guint)(minutes * 60000);\n 84 | }","lines":16,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/alarms.c#L69-84"}]},
{"id":"OVF_9","type":"overflow_underflow","type_label":"Overflow Underflow","severity":"low","difficulty":"easy","description":"Three places format a guint64 uid/window-id with g_strdup_printf(\"%ld\", ...). %ld is for signed long; on 32-bit platforms (where long is 32-bit) this truncates the 64-bit value and is also the wrong signedness, producing incorrect tab/window ids in tabs.onRemoved / windows.onRemoved JSON.","fix":"Use G_GUINT64_FORMAT (e.g. g_strdup_printf(\"%\" G_GUINT64_FORMAT, uid)).","locations":[{"ref":"src/webextension/ephy-web-extension-manager.c:1550","code":" 1550 | tab_json = g_strdup_printf (\"%ld\", ephy_web_view_get_uid (view));","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension-manager.c#L1550"},{"ref":"src/webextension/ephy-web-extension-manager.c:1607","code":" 1607 | g_autofree char *window_json = g_strdup_printf (\"%ld\", ephy_window_get_uid (window));","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension-manager.c#L1607"}]},
{"id":"OVF_10","type":"overflow_underflow","type_label":"Overflow Underflow","severity":"negligible","difficulty":"easy","description":"time_now_ms() mixes integer and double arithmetic: (spec.tv_sec * 1000) + (spec.tv_nsec / 1.0e6) is evaluated as a double then cast to guint64, losing precision unnecessarily.","fix":"Compute fully in integer: `return ((guint64)spec.tv_sec * 1000) + ((guint64)spec.tv_nsec / 1000000);`.","locations":[{"ref":"src/webextension/api/alarms.c:61-67","code":" 61 | static guint64\n 62 | time_now_ms (void)\n 63 | {\n 64 | struct timespec spec;\n 65 | clock_gettime (CLOCK_REALTIME, &spec);\n 66 | return (spec.tv_sec * 1000) + (spec.tv_nsec / 1.0e6);\n 67 | }","lines":7,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/alarms.c#L61-67"}]},
{"id":"LOGIC_1","type":"logic_error","type_label":"Logic Error","severity":"high","difficulty":"medium","description":"ephy_downloads_manager_add_download acquires the session inhibitor and connects the EphyDownload 'completed'/'error' signals, but if the download is already finished when added, those signals will never fire again, so release_session_inhibitor is never called. The inhibitor leaks, blocking session suspend/logout indefinitely.","fix":"In add_download, if the download is already finished, do not acquire the inhibitor (or release it immediately) and emit completion as appropriate.","locations":[{"ref":"embed/ephy-downloads-manager.c:170-198","code":" 170 | void\n 171 | ephy_downloads_manager_add_download (EphyDownloadsManager *manager,\n 172 | EphyDownload *download)\n 173 | {\n 174 | WebKitDownload *wk_download;\n 175 | \n 176 | g_assert (EPHY_IS_DOWNLOADS_MANAGER (manager));\n 177 | g_assert (EPHY_IS_DOWNLOAD (download));\n 178 | \n 179 | if (g_list_find (manager->downloads, download))\n 180 | return;\n 181 | \n 182 | ephy_downloads_manager_acquire_session_inhibitor (manager);\n 183 | \n 184 | manager->downloads = g_list_prepend (manager->downloads, g_object_ref (download));\n 185 | g_signal_connect (download, \"completed\",\n 186 | G_CALLBACK (download_completed_cb),\n 187 | manager);\n 188 | g_signal_connect (download, \"error\",\n 189 | G_CALLBACK (download_failed_cb),\n 190 | manager);\n 191 | \n 192 | wk_download = ephy_download_get_webkit_download (download);\n 193 | g_signal_connect_swapped (wk_download, \"notify::estimated-progress\",\n 194 | G_CALLBACK (download_estimated_progress_changed_cb),\n 195 | manager);\n 196 | g_signal_emit (manager, signals[DOWNLOAD_ADDED], 0, download);\n 197 | g_signal_emit (manager, signals[ESTIMATED_PROGRESS_CHANGED], 0);\n 198 | }","lines":29,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-downloads-manager.c#L170-198"}]},
{"id":"LOGIC_2","type":"logic_error","type_label":"Logic Error","severity":"high","difficulty":"easy","description":"In normalize_languages() the inner loop iterates with index j over sys_langs, but the array element is read with the OUTER index i: g_strdup (sys_langs[i]). When 'system' is the first language (i==0) it duplicates sys_langs[0] for every j and ignores the rest. When 'system' appears later (i>0), sys_langs[i] is read with the wrong/out-of-bounds index. This corrupts the spell-checking language list and can read out of bounds.","fix":"Use the inner index: g_strdup (sys_langs[j]).","locations":[{"ref":"embed/ephy-embed-prefs.c:334-335","code":" 334 | for (j = 0; sys_langs && sys_langs[j]; j++)\n 335 | g_ptr_array_add (langs, g_strdelimit (g_strdup (sys_langs[i]), \"-\", '_'));","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-embed-prefs.c#L334-335"}]},
{"id":"LOGIC_3","type":"logic_error","type_label":"Logic Error","severity":"high","difficulty":"medium","description":"When hush is enabled, the filters array is sized to num_filters = i+1, but manager->filters is allocated (or remains the NULLed pointer from clear_filters_list) and webkit_user_content_filter_store_save() for the hush filter is issued before the array is guaranteed allocated. filter_saved_cb writes manager->filters[self->index]; if it runs before the array exists this is a NULL/garbage write.","fix":"Allocate manager->filters (g_new0 sized to the final count) before issuing any save/load calls.","locations":[{"ref":"embed/ephy-filters-manager.c:1095-1110","code":" 1095 | webkit_user_content_filter_store_save (manager->store,\n 1096 | filter_info_get_identifier (filter_info),\n 1097 | data,\n 1098 | manager->cancellable,\n 1099 | (GAsyncReadyCallback)filter_saved_cb,\n 1100 | filter_info);\n 1101 | \n 1102 | /* Even if we already have a previous filter info, we still have to do all\n 1103 | * the work again anyway, because we have deleted the filter itself.\n 1104 | */\n 1105 | g_clear_pointer (&manager->hush_filter_info, filter_info_unref);\n 1106 | manager->hush_filter_info = g_steal_pointer (&filter_info);\n 1107 | }\n 1108 | \n 1109 | manager->num_filters = i + 1;\n 1110 | manager->filters = g_new0 (WebKitUserContentFilter *, manager->num_filters);","lines":16,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-filters-manager.c#L1095-1110"},{"ref":"embed/ephy-filters-manager.c:505-507","code":" 505 | if (self->is_hush)\n 506 | self->manager->hush_filter = filter;\n 507 | self->manager->filters[self->index] = g_steal_pointer (&filter);","lines":3,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-filters-manager.c#L505-507"}]},
{"id":"LOGIC_4","type":"logic_error","type_label":"Logic Error","severity":"high","difficulty":"medium","description":"on_frame_destroyed removes from view_contexts using &id (address of a stack int), but the table was populated with GUINT_TO_POINTER(frame_id) value-keys (g_direct_hash). The &id key never matches, so view_contexts entries (each holding a JSCContext ref) are never removed and leak for the extension lifetime; ephy_get_view_objects keeps returning stale/destroyed-frame contexts.","fix":"Remove using the same key representation: g_hash_table_remove (view_contexts, GUINT_TO_POINTER (webkit_frame_get_id (frame))); declare id as guint.","locations":[{"ref":"embed/web-process-extension/ephy-web-process-extension.c:971-980","code":" 971 | static void\n 972 | on_frame_destroyed (gpointer user_data,\n 973 | GObject *object)\n 974 | {\n 975 | EphyWebProcessExtension *extension = user_data;\n 976 | WebKitFrame *frame = WEBKIT_FRAME (object);\n 977 | int id = webkit_frame_get_id (frame);\n 978 | \n 979 | g_hash_table_remove (extension->view_contexts, &id);\n 980 | }","lines":10,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-web-process-extension.c#L971-980"},{"ref":"embed/web-process-extension/ephy-web-process-extension.c:1020-1027","code":" 1020 | /* The first context made is assumed to be the background page. */\n 1021 | if (!extension->background_context && extension_data->has_background_page)\n 1022 | extension->background_context = js_context;\n 1023 | \n 1024 | if (!g_hash_table_contains (extension->view_contexts, GUINT_TO_POINTER (webkit_frame_get_id (frame)))) {\n 1025 | g_hash_table_insert (extension->view_contexts, GUINT_TO_POINTER (webkit_frame_get_id (frame)), g_object_ref (js_context));\n 1026 | g_object_weak_ref (G_OBJECT (frame), on_frame_destroyed, extension);\n 1027 | }","lines":8,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-web-process-extension.c#L1020-1027"}]},
{"id":"LOGIC_5","type":"logic_error","type_label":"Logic Error","severity":"high","difficulty":"easy","description":"In ephy_langs_normalize_languages(), the 'system' expansion loop iterates sys_langs with index j but indexes sys_langs[i] (the OUTER loop variable) when duplicating: 'g_ptr_array_add (langs, g_strdelimit (g_strdup (sys_langs[i]), ...))'. This reads the wrong element and, since i indexes the caller's 'languages' array (unrelated length), can read out of bounds of sys_langs (NULL or garbage), and never adds the actual system languages correctly.","fix":"Use sys_langs[j] instead of sys_langs[i] inside the inner loop.","locations":[{"ref":"lib/ephy-langs.c:148-149","code":" 148 | for (j = 0; sys_langs && sys_langs[j]; j++)\n 149 | g_ptr_array_add (langs, g_strdelimit (g_strdup (sys_langs[i]), \"-\", '_'));","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/ephy-langs.c#L148-149"}]},
{"id":"LOGIC_6","type":"logic_error","type_label":"Logic Error","severity":"high","difficulty":"easy","description":"ephy_shell_get_lockdown() lazily creates shell->lockdown but then returns G_OBJECT (shell->session) instead of the lockdown object. The created lockdown object is leaked from the caller's perspective and the function returns the wrong (possibly NULL in some modes) object. Classic copy-paste bug.","fix":"Return G_OBJECT (shell->lockdown) instead of G_OBJECT (shell->session).","locations":[{"ref":"src/ephy-shell.c:904-913","code":" 904 | static GObject *\n 905 | ephy_shell_get_lockdown (EphyShell *shell)\n 906 | {\n 907 | g_assert (EPHY_IS_SHELL (shell));\n 908 | \n 909 | if (!shell->lockdown)\n 910 | shell->lockdown = g_object_new (EPHY_TYPE_LOCKDOWN, NULL);\n 911 | \n 912 | return G_OBJECT (shell->session);\n 913 | }","lines":10,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-shell.c#L904-913"}]},
{"id":"LOGIC_7","type":"logic_error","type_label":"Logic Error","severity":"high","difficulty":"easy","description":"set_permission() uses `else if (EPHY_PERMISSION_TYPE_ACCESS_WEBCAM_AND_MICROPHONE)` which tests a non-zero enum constant, so the branch is ALWAYS taken whenever the first `if` is false. The intent was clearly to compare data->permission_type against that enum value. As written, the webcam+microphone special-casing fires for unrelated permission types that aren't stored by the permissions manager.","fix":"Change the condition to `else if (data->permission_type == EPHY_PERMISSION_TYPE_ACCESS_WEBCAM_AND_MICROPHONE)`.","locations":[{"ref":"src/ephy-window.c:3242-3258","code":" 3242 | if (ephy_permission_is_stored_by_permissions_manager (data->permission_type)) {\n 3243 | ephy_permissions_manager_set_permission (permissions_manager,\n 3244 | data->permission_type, data->origin,\n 3245 | response ? EPHY_PERMISSION_PERMIT\n 3246 | : EPHY_PERMISSION_DENY);\n 3247 | } else if (EPHY_PERMISSION_TYPE_ACCESS_WEBCAM_AND_MICROPHONE) {\n 3248 | ephy_permissions_manager_set_permission (permissions_manager,\n 3249 | EPHY_PERMISSION_TYPE_ACCESS_WEBCAM,\n 3250 | data->origin,\n 3251 | response ? EPHY_PERMISSION_PERMIT\n 3252 | : EPHY_PERMISSION_DENY);\n 3253 | ephy_permissions_manager_set_permission (permissions_manager,\n 3254 | EPHY_PERMISSION_TYPE_ACCESS_MICROPHONE,\n 3255 | data->origin,\n 3256 | response ? EPHY_PERMISSION_PERMIT\n 3257 | : EPHY_PERMISSION_DENY);\n 3258 | }","lines":17,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-window.c#L3242-3258"}]},
{"id":"LOGIC_8","type":"logic_error","type_label":"Logic Error","severity":"high","difficulty":"easy","description":"filter_downloads() limit handling is broken. query->limit defaults to ephy_json_object_get_int() which returns -1 when 'limit' is absent. The guard `if (query->limit)` is TRUE for -1, so the trim path runs whenever limit is omitted. g_list_nth(matches, -1 + 1) == g_list_nth(matches, 0) returns the head node; g_list_remove_link(matches, head) returns the NEW list head into 'extras', then g_list_free(extras) frees the entire remaining list while 'matches' (which is returned) is left dangling. Result: downloads.search()/erase() with no 'limit' returns a freed/garbage list (use-after-free) and/or drops all results. Even with a real N, g_list_nth(matches, N+1) keeps N+1 items (off-by-one).","fix":"Only trim for a positive limit and unlink the tail correctly: `if (query->limit > 0) { GList *node = g_list_nth(matches, query->limit); if (node) { if (node->prev) node->prev->next = NULL; node->prev = NULL; g_list_free(node); } }` (do not reassign extras to the list head).","locations":[{"ref":"src/webextension/api/downloads.c:626-632","code":" 626 | if (query->limit) {\n 627 | extras = g_list_nth (matches, query->limit + 1);\n 628 | if (extras) {\n 629 | extras = g_list_remove_link (matches, extras);\n 630 | g_list_free (extras);\n 631 | }\n 632 | }","lines":7,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/downloads.c#L626-632"}]},
{"id":"LOGIC_9","type":"logic_error","type_label":"Logic Error","severity":"high","difficulty":"medium","description":"create_context_menu_item() attaches behavior to the single app-wide 'webextension-context-menu' GAction by g_signal_connect(action, \"activate\", ...) and g_object_set_data(action, \"command\", ...) every time a context menu is built, without ever disconnecting. Handlers accumulate without bound and the 'command' data is overwritten globally, so after opening N context menus one activation fires N callbacks against stale data.","fix":"Encode the command in the per-item GVariant payload and handle it in one persistent activate handler, or use a per-menu action destroyed with the menu.","locations":[{"ref":"src/webextension/api/menus.c:663-668","code":" 663 | if (item->command != COMMAND_NONE) {\n 664 | g_object_set_data (G_OBJECT (action), \"command\", GINT_TO_POINTER (item->command));\n 665 | g_signal_connect (action, \"activate\",\n 666 | G_CALLBACK (menu_activate_command_action),\n 667 | self);\n 668 | }","lines":6,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/menus.c#L663-668"},{"ref":"src/webextension/api/menus.c:699-700","code":" 699 | action = g_action_map_lookup_action (G_ACTION_MAP (ephy_shell_get_default ()), \"webextension-context-menu\");\n 700 | g_assert (action);","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/menus.c#L699-700"}]},
{"id":"LOGIC_10","type":"logic_error","type_label":"Logic Error","severity":"high","difficulty":"easy","description":"tabs_handler_remove() is missing return statements after the two success paths, so control always falls through to the final g_task_return_new_error(). When tab_ids is an array, the function calls g_task_return_pointer() then continues and ALSO calls g_task_return_new_error() on the same GTask (double-completion of a GTask is a fatal g_return assertion / crash). When tab_ids is a single int, the same double-return happens. Only the unreachable invalid-argument path is ever 'clean'. Any call to tabs.remove() double-completes the task.","fix":"Add 'return;' after each g_task_return_pointer() (inside the array branch after the loop, and after the single-id branch).","locations":[{"ref":"src/webextension/api/tabs.c:692-709","code":" 692 | if (JSON_NODE_HOLDS_ARRAY (tab_ids)) {\n 693 | JsonArray *array = json_node_get_array (tab_ids);\n 694 | \n 695 | for (guint i = 0; i < json_array_get_length (array); i++) {\n 696 | gint64 tab_id = ephy_json_array_get_int (array, i);\n 697 | if (tab_id != -1)\n 698 | close_tab_id (shell, tab_id);\n 699 | }\n 700 | \n 701 | g_task_return_pointer (task, NULL, NULL);\n 702 | }\n 703 | \n 704 | if ((tab_id = ephy_json_node_get_int (tab_ids)) != -1) {\n 705 | close_tab_id (shell, tab_id);\n 706 | g_task_return_pointer (task, NULL, NULL);\n 707 | }\n 708 | \n 709 | g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, \"tabs.remove(): First argument is not a number or array.\");","lines":18,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/tabs.c#L692-709"}]},
{"id":"LOGIC_11","type":"logic_error","type_label":"Logic Error","severity":"high","difficulty":"medium","description":"dialog_password_import_cb() calls show_import_export_result(parent, imported, imported, ...). The second argument is `destroy_parent`. Passing `imported` means that on a SUCCESSFUL password import the parent window (the active browser window passed as user_data to import_passwords_using_option_id) is destroyed when the result dialog is dismissed. Every other caller passes FALSE for destroy_parent. This closes the user's browser window after a successful Chrome/Chromium password import.","fix":"Pass FALSE for destroy_parent: show_import_export_result (parent, FALSE, imported, error, ...).","locations":[{"ref":"src/window-commands.c:773-784","code":" 773 | static void\n 774 | dialog_password_import_cb (GObject *source_object,\n 775 | GAsyncResult *res,\n 776 | gpointer user_data)\n 777 | {\n 778 | GtkWindow *parent = user_data;\n 779 | g_autoptr (GError) error = NULL;\n 780 | gboolean imported = ephy_password_import_from_chrome_finish (source_object, res, &error);\n 781 | \n 782 | show_import_export_result (parent, imported, imported, error,\n 783 | _(\"Passwords successfully imported!\"));\n 784 | }","lines":12,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/window-commands.c#L773-784"}]},
{"id":"LOGIC_12","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"easy","description":"certificate_selection_dialog_response_cb, when no matching slot is found, warns and returns WITHOUT calling cancel_authentication. This leaves the WebKitAuthenticationRequest pending forever, hanging the page. Every other failure path cancels.","fix":"Call cancel_authentication(self) before returning in the !slot branch.","locations":[{"ref":"embed/ephy-client-certificate-manager.c:366-369","code":" 366 | if (!slot) {\n 367 | g_warning (\"Unknown certificate label selected, abort!\");\n 368 | return;\n 369 | }","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-client-certificate-manager.c#L366-369"}]},
{"id":"LOGIC_13","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"easy","description":"Filename truncation truncates the ENTIRE destination path (directory + filename) to NAME_MAX, which is the per-component limit not the full-path limit, so it both fails to fix overlong basenames and can cut inside the directory. It also mixes byte/char semantics: g_utf8_strlen(s, NAME_MAX) is a char count over at most NAME_MAX bytes, then passed as a char end-index to g_utf8_substring.","fix":"Truncate only the basename component to NAME_MAX bytes on a UTF-8 boundary, leaving the directory untouched.","locations":[{"ref":"embed/ephy-download.c:239-244","code":" 239 | if (strlen (destination_filename) > NAME_MAX) {\n 240 | g_autofree char *truncated_filename = g_utf8_substring (destination_filename, 0,\n 241 | g_utf8_strlen (destination_filename, NAME_MAX));\n 242 | g_free (destination_filename);\n 243 | destination_filename = g_steal_pointer (&truncated_filename);\n 244 | }","lines":6,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-download.c#L239-244"}]},
{"id":"LOGIC_14","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"medium","description":"ephy_encodings_init de-duplicates the persisted recent list with g_slist_find() comparing pointers (char* identity), not string contents, so it never detects duplicates and relies only on the RECENT_MAX cap - contradicting the dedup intent. The add path correctly uses g_slist_find_custom with strcmp.","fix":"Use g_slist_find_custom (encodings->recent, item, (GCompareFunc)strcmp).","locations":[{"ref":"embed/ephy-encodings.c:380-384","code":" 380 | if (!g_slist_find (encodings->recent, item)\n 381 | && g_slist_length (encodings->recent) < RECENT_MAX\n 382 | && ephy_encodings_get_encoding (encodings, item, FALSE)) {\n 383 | encodings->recent = g_slist_prepend (encodings->recent, g_strdup (item));\n 384 | }","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-encodings.c#L380-384"}]},
{"id":"LOGIC_15","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"medium","description":"filter_info_setup_done unconditionally dereferences self->manager (g_hash_table_foreach on self->manager->filter_infos), unlike sibling callbacks that guard `if (!self->manager) return;`. filter_info_unref clears manager on last unref, so a setup-done for a FilterInfo whose manager was cleared is a NULL deref. accumulate_filter_done has the same exposure.","fix":"Add `if (!self->manager) return;` at the top of filter_info_setup_done.","locations":[{"ref":"embed/ephy-filters-manager.c:803-831","code":" 803 | static void\n 804 | accumulate_filter_done (const char *identifier,\n 805 | FilterInfo *filter,\n 806 | gboolean *done)\n 807 | {\n 808 | g_assert (strcmp (identifier, filter_info_get_identifier (filter)) == 0);\n 809 | g_assert (g_hash_table_contains (filter->manager->filter_infos, identifier));\n 810 | \n 811 | *done = *done && filter->done;\n 812 | }\n 813 | \n 814 | static void\n 815 | filter_info_setup_done (FilterInfo *self)\n 816 | {\n 817 | gboolean done = self->done = TRUE;\n 818 | \n 819 | g_hash_table_foreach (self->manager->filter_infos,\n 820 | (GHFunc)accumulate_filter_done,\n 821 | &done);\n 822 | \n 823 | LOG (\"Setup for filter %s from <%s> completed.\",\n 824 | filter_info_get_identifier (self), self->source_uri);\n 825 | \n 826 | if (done) {\n 827 | LOG (\"Setup completed for %u filters.\",\n 828 | g_hash_table_size (self->manager->filter_infos));\n 829 | filters_manager_ensure_initialized (self->manager);\n 830 | }\n 831 | }","lines":29,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-filters-manager.c#L803-831"}]},
{"id":"LOGIC_16","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"medium","description":"update_adblock_filter_files_cb sets manager->num_filters = i+1 unconditionally even when hush is disabled, mis-sizing the count vs. the real number of filters; the hush FilterInfo is also never inserted into manager->filter_infos, so the done-accounting (filter_info_setup_done iterating filter_infos) can report 'done' before the hush filter finishes.","fix":"Track the actual filter count and either insert the hush FilterInfo into filter_infos or exclude it from done-accounting explicitly.","locations":[{"ref":"embed/ephy-filters-manager.c:1085-1110","code":" 1085 | if (hush_enabled) {\n 1086 | g_autoptr (GBytes) data = g_resources_lookup_data (\"/org/gnome/epiphany/hush.json\", 0, NULL);\n 1087 | g_autofree char *filter_id = filter_info_identifier_for_source_uri (\"org/gnome/epiphany/hush.json\");\n 1088 | FilterInfo *filter_info = NULL;\n 1089 | \n 1090 | filter_info = filter_info_new (\"/org/gnome/epiphany/hush.json\", manager);\n 1091 | filter_info->identifier = g_steal_pointer (&filter_id);\n 1092 | filter_info->index = i; /* already incremented by termination of the for loop */\n 1093 | filter_info->is_hush = TRUE;\n 1094 | \n 1095 | webkit_user_content_filter_store_save (manager->store,\n 1096 | filter_info_get_identifier (filter_info),\n 1097 | data,\n 1098 | manager->cancellable,\n 1099 | (GAsyncReadyCallback)filter_saved_cb,\n 1100 | filter_info);\n 1101 | \n 1102 | /* Even if we already have a previous filter info, we still have to do all\n 1103 | * the work again anyway, because we have deleted the filter itself.\n 1104 | */\n 1105 | g_clear_pointer (&manager->hush_filter_info, filter_info_unref);\n 1106 | manager->hush_filter_info = g_steal_pointer (&filter_info);\n 1107 | }\n 1108 | \n 1109 | manager->num_filters = i + 1;\n 1110 | manager->filters = g_new0 (WebKitUserContentFilter *, manager->num_filters);","lines":26,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-filters-manager.c#L1085-1110"},{"ref":"embed/ephy-filters-manager.c:814-831","code":" 814 | static void\n 815 | filter_info_setup_done (FilterInfo *self)\n 816 | {\n 817 | gboolean done = self->done = TRUE;\n 818 | \n 819 | g_hash_table_foreach (self->manager->filter_infos,\n 820 | (GHFunc)accumulate_filter_done,\n 821 | &done);\n 822 | \n 823 | LOG (\"Setup for filter %s from <%s> completed.\",\n 824 | filter_info_get_identifier (self), self->source_uri);\n 825 | \n 826 | if (done) {\n 827 | LOG (\"Setup completed for %u filters.\",\n 828 | g_hash_table_size (self->manager->filter_infos));\n 829 | filters_manager_ensure_initialized (self->manager);\n 830 | }\n 831 | }","lines":18,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-filters-manager.c#L814-831"}]},
{"id":"LOGIC_17","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"medium","description":"readability_js_finish_cb dereferences style_css via g_bytes_get_data without checking that g_resources_lookup_data for reader.css succeeded (NULL error out-param, result unchecked). If the resource is missing, style_css is NULL and g_bytes_get_data(NULL,...) yields NULL printed via %s or crashes. The Readability.js lookup IS checked; the CSS one is not.","fix":"Check style_css for NULL after g_resources_lookup_data and handle failure.","locations":[{"ref":"embed/ephy-reader-handler.c:187","code":" 187 | style_css = g_resources_lookup_data (\"/org/gnome/epiphany/readability/reader.css\", G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-reader-handler.c#L187"},{"ref":"embed/ephy-reader-handler.c:216","code":" 216 | (gchar *)g_bytes_get_data (style_css, NULL),","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-reader-handler.c#L216"}]},
{"id":"LOGIC_18","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"easy","description":"Copy-paste bug in add_to_totals(): 'totals->private_dirty += entry->private_dirty;' is executed twice and 'totals->private_clean' is never added. The Private Clean column in the memory totals row is always 0 and Private Dirty is double-counted.","fix":"Change the third line to 'totals->private_clean += entry->private_clean;'.","locations":[{"ref":"lib/ephy-smaps.c:151-155","code":" 151 | totals->shared_clean += entry->shared_clean;\n 152 | totals->shared_dirty += entry->shared_dirty;\n 153 | totals->private_dirty += entry->private_dirty;\n 154 | totals->private_dirty += entry->private_dirty;\n 155 | }","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/ephy-smaps.c#L151-155"}]},
{"id":"LOGIC_19","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"easy","description":"ephy_history_record_visit_compare returns (visit2->timestamp - visit1->timestamp) where both timestamps are gint64 (microseconds), but the function returns int. The 64-bit difference is truncated to 32 bits, so for timestamps differing by more than ~2^31 microseconds (~35 minutes) the sign of the comparison can be wrong, breaking the sort order and the g_sequence_lookup/insert_sorted invariant (duplicate detection and 'most recent visit' may be incorrect).","fix":"Return the sign explicitly: `return (visit2->timestamp > visit1->timestamp) - (visit2->timestamp < visit1->timestamp);`","locations":[{"ref":"lib/sync/ephy-history-record.c:80-90","code":" 80 | static int\n 81 | ephy_history_record_visit_compare (EphyHistoryRecordVisit *visit1,\n 82 | EphyHistoryRecordVisit *visit2,\n 83 | gpointer user_data)\n 84 | {\n 85 | g_assert (visit1);\n 86 | g_assert (visit2);\n 87 | \n 88 | /* We keep visits sorted in descending order by timestamp. */\n 89 | return visit2->timestamp - visit1->timestamp;\n 90 | }","lines":11,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-history-record.c#L80-90"}]},
{"id":"LOGIC_20","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"easy","description":"decrypt()'s padding removal loop zeroes EVERY non-printable byte anywhere in the decrypted plaintext, not just the trailing PKCS#7 padding. Because the result is then used as a NUL-terminated string, any password containing a legitimate non-ASCII/UTF-8 byte is silently truncated at the first such byte, corrupting imported passwords. It also does not validate the PKCS#7 pad length.","fix":"Strip PKCS#7 padding using the last byte: n = out[password_length-1]; validate 1 <= n <= AES_BLOCK_SIZE; set out[password_length - n] = '\\0'. Do not scan/zero interior bytes.","locations":[{"ref":"lib/sync/ephy-password-import.c:99-103","code":" 99 | /* Remove padding */\n 100 | for (int i = 0; i < password_length; i++) {\n 101 | if (!g_ascii_isprint (out[i]))\n 102 | out[i] = '\\0';\n 103 | }","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-password-import.c#L99-103"}]},
{"id":"LOGIC_21","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"medium","description":"When opening an ephy-tab:// URI that targets a different window (window_id != 0), `webview` is captured from the CURRENT window's selected page (lines 94-95) before tab_view is switched to the target window. After switching tab_view and selecting the target tab, the code grabs focus on the stale `webview` (wrong window's page) and tests ephy_web_view_is_overview() on that stale view, then closes the stale `tab`. Cross-window tab activation focuses/closes the wrong tab.","fix":"After selecting the destination tab in the (possibly remote) tab_view, re-fetch the selected page and its web view before grabbing focus / testing overview / closing.","locations":[{"ref":"src/ephy-location-controller.c:93-125","code":" 93 | tab = ephy_tab_view_get_selected_page (tab_view);\n 94 | webview = ephy_embed_get_web_view (EPHY_EMBED (tab));\n 95 | \n 96 | if (window_id != 0) {\n 97 | GApplication *application;\n 98 | EphyEmbedShell *shell;\n 99 | EphyWindow *window;\n 100 | GList *windows;\n 101 | \n 102 | shell = ephy_embed_shell_get_default ();\n 103 | application = G_APPLICATION (shell);\n 104 | windows = gtk_application_get_windows (GTK_APPLICATION (application));\n 105 | \n 106 | if ((guint)window_id >= g_list_length (windows))\n 107 | return FALSE;\n 108 | \n 109 | window = g_list_nth_data (windows, window_id);\n 110 | tab_view = ephy_window_get_tab_view (window);\n 111 | \n 112 | gtk_window_present (GTK_WINDOW (window));\n 113 | }\n 114 | \n 115 | if (tab_id >= ephy_tab_view_get_n_pages (tab_view))\n 116 | return FALSE;\n 117 | \n 118 | ephy_tab_view_select_nth_page (tab_view, tab_id);\n 119 | gtk_widget_grab_focus (GTK_WIDGET (webview));\n 120 | \n 121 | if (ephy_web_view_is_overview (webview)) {\n 122 | if (window_id != 0)\n 123 | tab_view = ephy_window_get_tab_view (controller->window);\n 124 | ephy_tab_view_close (tab_view, tab);\n 125 | }","lines":33,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-location-controller.c#L93-125"}]},
{"id":"LOGIC_22","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"medium","description":"update_url_button_style(): the LOG() error message at the 'no base domain' goto uses error->message, but at this point 'error' is the GError from the earlier g_uri_parse() call which succeeded (error == NULL there). Dereferencing error->message when base_domain is NULL but the URI parsed fine is a NULL dereference inside the LOG macro argument evaluation (only triggered when LOG/debug is enabled).","fix":"Remove error->message from this LOG call (the failure here is from ephy_uri_get_base_domain, which sets no GError), e.g. log only base domain/text.","locations":[{"ref":"src/ephy-location-entry.c:386-390","code":" 386 | base_domain = ephy_uri_get_base_domain (host);\n 387 | if (!base_domain) {\n 388 | /* It's probably not in the public suffix list. */\n 389 | LOG (\"Failed to update URL button style: failed to get base domain for URL %s: %s\", text, error->message);\n 390 | goto out;","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-location-entry.c#L386-390"}]},
{"id":"LOGIC_23","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"medium","description":"finish_closing_after_modified_forms_check() iterates the list returned by gtk_application_get_windows() while calling ephy_window_handle_quit_with_modified_forms() on each window, which can force-close/destroy the window. Destroying a GtkApplication window removes it from the very GList being walked, freeing the list node; `windows = windows->next` was captured before the call, but the node it points to can also be freed if a destroyed window's removal mutates neighbouring nodes. Safer to snapshot the list first. (Same pattern risk in ephy_shell_close_all_windows, which re-fetches the list before its destroy loop and is OK.)","fix":"Copy the window list (g_list_copy) before iterating and destroying, then free the copy; do not rely on live GtkApplication list nodes across window destruction.","locations":[{"ref":"src/ephy-shell.c:1497-1514","code":" 1497 | static void\n 1498 | finish_closing_after_modified_forms_check (EphyShell *shell)\n 1499 | {\n 1500 | GList *windows = gtk_application_get_windows (GTK_APPLICATION (shell));\n 1501 | EphySession *session = ephy_shell_get_session (shell);\n 1502 | \n 1503 | if (session)\n 1504 | ephy_session_close (session);\n 1505 | \n 1506 | shell->checking_modified_forms = FALSE;\n 1507 | while (windows) {\n 1508 | EphyWindow *window = EPHY_WINDOW (windows->data);\n 1509 | \n 1510 | windows = windows->next;\n 1511 | ephy_window_handle_quit_with_modified_forms (window);\n 1512 | }\n 1513 | g_application_quit (G_APPLICATION (shell));\n 1514 | }","lines":18,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-shell.c#L1497-1514"}]},
{"id":"LOGIC_24","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"medium","description":"Partial / non-atomic migration in migrate_pre_flatpak_webapps(): the profile directory is renamed first (line 136), then several later steps (reading the old desktop file at 162, loading it as a key file at 168, saving the new desktop file at 201, deleting the old one at 207, renaming the icon at 189) can each fail and `goto next`, leaving the app half-migrated - directory renamed to the new ${APP_ID}.WebApp_${hash} name while the .desktop file, icon and symlink still reference the old name. Subsequent runs cannot recover because the directory no longer starts with the org.gnome.Epiphany.WebApp- prefix that the migration loop matches on (line 94), so the broken state is permanent. Notably the icon g_rename at line 189 moves the icon out of the profile before the new desktop file is even saved; if the later save/delete fails the icon is orphaned.","fix":"Perform the rename of the profile directory last, after the new desktop file and icon have been successfully written, or roll back the directory rename on any failure so a re-run can retry. At minimum, defer the icon g_rename and old-desktop-file delete until after the new desktop file has been saved successfully.","locations":[{"ref":"src/profile-migrator/ephy-profile-migrator.c:134-233","code":" 134 | /* Rename profile directory */\n 135 | old_desktop_path_name = g_strconcat (parent_directory_path, G_DIR_SEPARATOR_S, name, NULL);\n 136 | new_desktop_path_name = g_strconcat (parent_directory_path, G_DIR_SEPARATOR_S, new_name, NULL);\n 137 | LOG (\"migrate_profile_directories: moving '%s' to '%s'\", old_desktop_path_name, new_desktop_path_name);\n 138 | if (g_rename (old_desktop_path_name, new_desktop_path_name) == -1) {\n 139 | g_warning (\"Cannot rename web application directory from '%s' to '%s'\", old_desktop_path_name, new_desktop_path_name);\n 140 | goto next;\n 141 | }\n 142 | \n 143 | /* Rename cache directory */\n 144 | old_cache_path = g_strconcat (g_get_user_cache_dir (), G_DIR_SEPARATOR_S, name, NULL);\n 145 | new_cache_path = g_strconcat (g_get_user_cache_dir (), G_DIR_SEPARATOR_S, new_name, NULL);\n 146 | if (g_file_test (old_cache_path, G_FILE_TEST_IS_DIR) &&\n 147 | g_rename (old_cache_path, new_cache_path) == -1) {\n 148 | g_warning (\"Cannot rename web application cache directory from '%s' to '%s'\", old_cache_path, new_cache_path);\n 149 | }\n 150 | \n 151 | /* Rename config directory */\n 152 | old_config_path = g_strconcat (g_get_user_config_dir (), G_DIR_SEPARATOR_S, name, NULL);\n 153 | new_config_path = g_strconcat (g_get_user_config_dir (), G_DIR_SEPARATOR_S, new_name, NULL);\n 154 | if (g_file_test (old_config_path, G_FILE_TEST_IS_DIR) &&\n 155 | g_rename (old_config_path, new_config_path) == -1) {\n 156 | g_warning (\"Cannot rename web application config directory from '%s' to '%s'\", old_config_path, new_config_path);\n 157 | }\n 158 | \n 159 | /* Create new desktop file */\n 160 | old_desktop_file_name = g_strconcat (parent_directory_path, G_DIR_SEPARATOR_S, new_name, G_DIR_SEPARATOR_S, name, \".desktop\", NULL);\n 161 | new_desktop_file_name = g_strconcat (portal_desktop_dir, G_DIR_SEPARATOR_S, new_name, \".desktop\", NULL);\n 162 | \n 163 | /* Fix paths in desktop file */\n 164 | if (!g_file_get_contents (old_desktop_file_name, &old_data, NULL, &error)) {\n 165 | g_warning (\"Cannot read contents of '%s': %s\", old_desktop_file_name, error->message);\n 166 | goto next;\n 167 | }\n 168 | new_data = ephy_string_find_and_replace ((const char *)old_data, name, new_name);\n 169 | keyfile = g_key_file_new ();\n 170 | if (!g_key_file_load_from_data (keyfile, new_data, -1,\n 171 | G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS,\n 172 | &error)) {\n 173 | g_warning (\"Cannot load old desktop file %s as key file: %s\", old_desktop_file_name, error->message);\n 174 | g_warning (\"Key file contents:\\n%s\\n\", (const char *)new_data);\n 175 | goto next;\n 176 | }\n 177 | /* The StartupWMClass will not always have been corrected by the\n 178 | * find-and-replace above, since it has the form\n 179 | * org.gnome.Epiphany.WebApp-<normalized_name>-<checksum> whereas the\n 180 | * gapplication id sometimes omits the <normalized_name> part\n 181 | */\n 182 | g_key_file_set_string (keyfile, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_STARTUP_WM_CLASS, new_name);\n 183 | \n 184 | /* Correct the Icon= key */\n 185 | icon = g_key_file_get_string (keyfile, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, &error);\n 186 | if (!icon) {\n 187 | g_warning (\"Failed to get Icon key from %s: %s\", old_desktop_file_name, error->message);\n 188 | g_clear_error (&error);\n 189 | } else if (g_str_has_suffix (icon, EPHY_WEB_APP_ICON_NAME)) {\n 190 | new_icon = g_strconcat (portal_icon_dir, G_DIR_SEPARATOR_S, new_name, \".png\", NULL);\n 191 | if (g_rename (icon, new_icon) == -1) {\n 192 | g_warning (\"Cannot rename icon from '%s' to '%s'\", icon, new_icon);\n 193 | goto next;\n 194 | }\n 195 | LOG (\"migrate_profile_directories: setting Icon to %s\", new_icon);\n 196 | g_key_file_set_string (keyfile, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, new_icon);\n 197 | }\n 198 | \n 199 | tryexec = g_key_file_get_string (keyfile, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, NULL);\n 200 | if (!tryexec)\n 201 | g_key_file_set_string (keyfile, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, \"epiphany\");\n 202 | \n 203 | if (!g_key_file_save_to_file (keyfile, new_desktop_file_name, &error)) {\n 204 | g_warning (\"Failed to save desktop file %s\", error->message);\n 205 | goto next;\n 206 | }\n 207 | \n 208 | old_desktop_file = g_file_new_for_path (old_desktop_file_name);\n 209 | if (!g_file_delete (old_desktop_file, NULL, &error)) {\n 210 | g_warning (\"Cannot delete old desktop file: %s\", error->message);\n 211 | goto next;\n 212 | }\n 213 | \n 214 | /* Remove old symlink */\n 215 | app_desktop_file_name = g_strconcat (g_get_user_data_dir (), G_DIR_SEPARATOR_S, \"applications\",\n 216 | G_DIR_SEPARATOR_S, name, \".desktop\", NULL);\n 217 | if (g_remove (app_desktop_file_name) == -1) {\n 218 | g_warning (\"Cannot remove old desktop file symlink '%s'\", app_desktop_file_name);\n 219 | goto next;\n 220 | }\n 221 | \n 222 | /* Create new symlink */\n 223 | g_free (app_desktop_file_name);\n 224 | app_desktop_file_name = g_strconcat (g_get_user_data_dir (), G_DIR_SEPARATOR_S, \"applications\",\n 225 | G_DIR_SEPARATOR_S, new_name, \".desktop\", NULL);\n 226 | app_link_file = g_file_new_for_path (app_desktop_file_name);\n 227 | relative_path = g_strconcat (\"..\", G_DIR_SEPARATOR_S, \"xdg-desktop-portal\", G_DIR_SEPARATOR_S,\n 228 | \"applications\", G_DIR_SEPARATOR_S, new_name, \".desktop\", NULL);\n 229 | \n 230 | if (!g_file_make_symbolic_link (app_link_file, relative_path, NULL, &error)) {\n 231 | g_warning (\"Cannot create symlink to new desktop file %s: %s\", new_desktop_file_name, error->message);\n 232 | goto next;\n 233 | }","lines":100,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/profile-migrator/ephy-profile-migrator.c#L134-233"}]},
{"id":"LOGIC_25","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"medium","description":"migrate_pre_flatpak_webapps() aborts the per-app migration with `goto next` if removing the OLD .desktop symlink fails (g_remove of app_desktop_file_name). By that point the profile/cache/config directories have already been renamed, the new portal .desktop file has been written and the old one deleted, but because of the early abort the NEW launcher symlink (lines 220-231) is never created. The result is an installed web app whose profile has been fully migrated but which has no working .desktop launcher in ~/.local/share/applications. The old symlink failing to remove (e.g. it was already gone) is not a fatal condition and should not skip creation of the new link.","fix":"Treat a failed removal of the stale symlink as a warning only (do not `goto next`); proceed to create the new symlink. Optionally unlink any pre-existing entry at the new path first so g_file_make_symbolic_link does not fail with EEXIST.","locations":[{"ref":"src/profile-migrator/ephy-profile-migrator.c:217-233","code":" 217 | if (g_remove (app_desktop_file_name) == -1) {\n 218 | g_warning (\"Cannot remove old desktop file symlink '%s'\", app_desktop_file_name);\n 219 | goto next;\n 220 | }\n 221 | \n 222 | /* Create new symlink */\n 223 | g_free (app_desktop_file_name);\n 224 | app_desktop_file_name = g_strconcat (g_get_user_data_dir (), G_DIR_SEPARATOR_S, \"applications\",\n 225 | G_DIR_SEPARATOR_S, new_name, \".desktop\", NULL);\n 226 | app_link_file = g_file_new_for_path (app_desktop_file_name);\n 227 | relative_path = g_strconcat (\"..\", G_DIR_SEPARATOR_S, \"xdg-desktop-portal\", G_DIR_SEPARATOR_S,\n 228 | \"applications\", G_DIR_SEPARATOR_S, new_name, \".desktop\", NULL);\n 229 | \n 230 | if (!g_file_make_symbolic_link (app_link_file, relative_path, NULL, &error)) {\n 231 | g_warning (\"Cannot create symlink to new desktop file %s: %s\", new_desktop_file_name, error->message);\n 232 | goto next;\n 233 | }","lines":17,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/profile-migrator/ephy-profile-migrator.c#L217-233"}]},
{"id":"LOGIC_26","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"easy","description":"total_bytes_less filter uses '>' instead of '<'. `if (query->total_bytes_less != -1 && (guint64)query->total_bytes_less > received_size) continue;` skips downloads SMALLER than the threshold, i.e. it keeps only downloads >= totalBytesLess - the opposite of the intended 'total bytes less than' semantics.","fix":"Use `(guint64)query->total_bytes_less < received_size` (skip when size is not less than the threshold).","locations":[{"ref":"src/webextension/api/downloads.c:602-603","code":" 602 | if (query->total_bytes_less != -1 && (guint64)query->total_bytes_less > received_size)\n 603 | continue;","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/downloads.c#L602-603"}]},
{"id":"LOGIC_27","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"medium","description":"Menu ids are assumed unique tree-wide, but insert keys children by id only within their parent hash table (value destroy = menu_item_free). An extension supplying a duplicate id (untrusted JSON) at a different nesting level silently frees/replaces the existing item, and id lookups become ambiguous.","fix":"Reject menus.create() when the id already exists anywhere in the tree (recursive search before insert), matching Firefox.","locations":[{"ref":"src/webextension/api/menus.c:279-305","code":" 279 | static gboolean\n 280 | insert_menu_item (GHashTable *menus,\n 281 | MenuItem *item)\n 282 | {\n 283 | MenuItem *parent;\n 284 | MenuItem *next_item;\n 285 | GHashTableIter iter;\n 286 | \n 287 | if (!item->parent_id) {\n 288 | g_hash_table_replace (menus, item->id, item);\n 289 | return TRUE;\n 290 | }\n 291 | \n 292 | parent = g_hash_table_lookup (menus, item->parent_id);\n 293 | if (parent) {\n 294 | g_hash_table_replace (parent->children, item->id, item);\n 295 | return TRUE;\n 296 | }\n 297 | \n 298 | g_hash_table_iter_init (&iter, menus);\n 299 | while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&next_item)) {\n 300 | if (insert_menu_item (next_item->children, item))\n 301 | return TRUE;\n 302 | }\n 303 | \n 304 | return FALSE;\n 305 | }","lines":27,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/menus.c#L279-305"},{"ref":"src/webextension/api/menus.c:334-348","code":" 334 | item = menu_item_new (create_properties);\n 335 | \n 336 | if (!item->id || (!item->title && item->menu_type != MENU_TYPE_SEPARATOR)) {\n 337 | g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, \"menus.create(): createProperties is missing an id or title\");\n 338 | menu_item_free (item);\n 339 | return;\n 340 | }\n 341 | \n 342 | if (!insert_menu_item (menus, item)) {\n 343 | g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, \"menus.create(): parentId not found\");\n 344 | menu_item_free (item);\n 345 | return;\n 346 | }\n 347 | \n 348 | g_task_return_pointer (task, g_strdup_printf (\"\\\"%s\\\"\", item->id), g_free);","lines":15,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/menus.c#L334-348"}]},
{"id":"LOGIC_28","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"medium","description":"tabs_handler_set_zoom() and tabs_handler_insert_css()/remove_css()/execute_script()/update() distinguish the optional-tabId overload by checking whether ephy_json_array_get_double/get_object on index 1 succeeded. For setZoom, zoom_level is read from index 1; if that is missing it returns -1.0 and is re-read from index 0. But a legitimate zoomFactor of exactly -1.0 cannot occur (range 0.3-5) so this is fine for zoom; however the value -1.0 sentinel collides with ephy_json_array_get_double's 'not a number' return, so passing a non-number at index 1 silently shifts to single-arg interpretation rather than erroring. Minor robustness issue / spec divergence.","fix":"Distinguish the two overloads by argument count / node type (ephy_json_array_get_element index 1 == NULL) instead of by sentinel value.","locations":[{"ref":"src/webextension/api/tabs.c:724-728","code":" 724 | zoom_level = ephy_json_array_get_double (args, 1);\n 725 | if (zoom_level == -1.0)\n 726 | zoom_level = ephy_json_array_get_double (args, 0);\n 727 | else\n 728 | tab_id = ephy_json_array_get_int (args, 0);","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/tabs.c#L724-728"}]},
{"id":"LOGIC_29","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"medium","description":"The RTL branch of the indicator placement uses size.height where size.width is intended for the horizontal offset (the LTR branch uses size.width with a size.height/2 nudge). RTL placement is therefore wrong.","fix":"Mirror the LTR computation using size.width, e.g. x = -size.width + size.height / 2.0f (verify against intended visual mirror).","locations":[{"ref":"src/webextension/ephy-indicator-bin.c:142-145","code":" 142 | else if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)\n 143 | x = -size.height / 2.0f;\n 144 | else\n 145 | x = width - size.width + size.height / 2.0f;","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-indicator-bin.c#L142-145"}]},
{"id":"LOGIC_30","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"medium","description":"window_cmd_import_bookmarks(): two bugs. (1) g_list_model_get_item (dialogs, i) returns a new reference (transfer full) into `dialog`, but it is never unreffed - a GObject ref is leaked on every iteration. (2) The dialog title is compared against the untranslated literal \"Import Bookmarks\", but import_bookmarks_using_option_dialog() set the title with _(\"Import Bookmarks\"); under a non-English locale the comparison never matches, so the existing dialog is never detected and a duplicate is presented each time.","fix":"Unref each item (g_autoptr or g_object_unref after use). Compare against a stable identifier instead of the translated title (e.g. set/check via g_object_set_data, or compare with _(\"Import Bookmarks\")).","locations":[{"ref":"src/window-commands.c:615-635","code":" 615 | else {\n 616 | GListModel *dialogs = adw_application_window_get_dialogs (ADW_APPLICATION_WINDOW (window));\n 617 | int i = 0;\n 618 | int n = g_list_model_get_n_items (dialogs);\n 619 | gboolean presented = FALSE;\n 620 | AdwDialog *dialog;\n 621 | \n 622 | for (i = 0; i < n; i++) {\n 623 | dialog = g_list_model_get_item (dialogs, i);\n 624 | \n 625 | if (g_strcmp0 (adw_dialog_get_title (dialog), \"Import Bookmarks\") == 0) {\n 626 | presented = TRUE;\n 627 | break;\n 628 | }\n 629 | }\n 630 | \n 631 | if (!presented)\n 632 | import_bookmarks_using_option_dialog (window);\n 633 | else\n 634 | adw_dialog_present (dialog, GTK_WIDGET (window));\n 635 | }","lines":21,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/window-commands.c#L615-635"}]},
{"id":"LOGIC_31","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"easy","description":"ephy_embed_utils_address_has_web_scheme computes colonpos via pointer subtraction of strstr() result; when there is no ':' strstr returns NULL and colonpos becomes (int)(0 - address), an arbitrary truncated value, making the subsequent 'colonpos < 0' check unreliable on 64-bit.","fix":"Use const char *colon = strchr(address, ':'); if (!colon) return FALSE; colonpos = colon - address;","locations":[{"ref":"embed/ephy-embed-utils.c:137-140","code":" 137 | colonpos = (int)((strstr (address, \":\")) - address);\n 138 | \n 139 | if (colonpos < 0)\n 140 | return FALSE;","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-embed-utils.c#L137-140"}]},
{"id":"LOGIC_32","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"ephy_embed_utils_address_is_valid treats any single token ending in '/' (e.g. 'foobar/') as a navigable host rather than a search term, because is_host_with_port returns TRUE for trailing-slash strings without verifying a plausible host before the slash.","fix":"Only treat trailing-slash input as a host when the part before '/' looks like a host (contains '.', or is localhost/IP).","locations":[{"ref":"embed/ephy-embed-utils.c:205-222","code":" 205 | static gboolean\n 206 | is_host_with_port (const char *address)\n 207 | {\n 208 | g_auto (GStrv) split = NULL;\n 209 | gint64 port = 0;\n 210 | \n 211 | if (strchr (address, ' '))\n 212 | return FALSE;\n 213 | \n 214 | if (g_str_has_suffix (address, \"/\"))\n 215 | return TRUE;\n 216 | \n 217 | split = g_strsplit (address, \":\", -1);\n 218 | if (g_strv_length (split) == 2)\n 219 | port = g_ascii_strtoll (split[1], NULL, 10);\n 220 | \n 221 | return port != 0;\n 222 | }","lines":18,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-embed-utils.c#L205-222"}]},
{"id":"LOGIC_33","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"filter_info_load_from_bytes uses g_variant_new_from_bytes with trusted=TRUE on on-disk sidecar bytes; if the file is corrupt/truncated g_variant_get with format \"(usmsx)\" can read out of bounds or return garbage rather than erroring. The NULL check on the variant is effectively dead (never NULL for a definite type). Also last_update is read into a guint64 against the 'x' (gint64) typecode.","fix":"Pass trusted=FALSE (or validate with g_variant_is_normal_form) and declare last_update as gint64.","locations":[{"ref":"embed/ephy-filters-manager.c:152-182","code":" 152 | guint64 last_update = 0;\n 153 | \n 154 | g_autoptr (GVariantType) value_type = g_variant_type_new (FILTER_INFO_VARIANT_FORMAT);\n 155 | g_autoptr (GVariant) value = g_variant_ref_sink (g_variant_new_from_bytes (value_type, data, TRUE));\n 156 | \n 157 | if (!value) {\n 158 | g_set_error_literal (error,\n 159 | G_IO_ERROR,\n 160 | G_IO_ERROR_INVALID_ARGUMENT,\n 161 | \"Cannot decode GVariant from bytes\");\n 162 | return FALSE;\n 163 | }\n 164 | \n 165 | g_variant_get_child (value, 0, \"u\", &saved_version);\n 166 | if (saved_version != FILTER_INFO_VARIANT_VERSION) {\n 167 | g_set_error (error,\n 168 | G_IO_ERROR,\n 169 | G_IO_ERROR_INVALID_DATA,\n 170 | \"Attempted to decode content filter data GVariant with\"\n 171 | \" format version %\" PRIu32 \" (expected %\" PRIu32 \")\",\n 172 | saved_version,\n 173 | FILTER_INFO_VARIANT_VERSION);\n 174 | return FALSE;\n 175 | }\n 176 | \n 177 | g_variant_get (value,\n 178 | FILTER_INFO_VARIANT_FORMAT,\n 179 | NULL, /* Ignore the version, it has been checked already. */\n 180 | &source_uri,\n 181 | &checksum,\n 182 | &last_update);","lines":31,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-filters-manager.c#L152-182"}]},
{"id":"LOGIC_34","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"on_network_metered updates manager->metered but the periodic update timeout (created once in constructed with the metered-or-not frequency) is never rescheduled, so a device that becomes metered/unmetered keeps the original refresh interval forever. The handler also re-fetches the default monitor instead of using its `monitor` argument.","fix":"Reschedule the update timeout on metered-state change and use the passed-in monitor argument.","locations":[{"ref":"embed/ephy-filters-manager.c:1152-1159","code":" 1152 | static void\n 1153 | on_network_metered (GNetworkMonitor *monitor,\n 1154 | GParamSpec *pspec,\n 1155 | EphyFiltersManager *manager)\n 1156 | \n 1157 | {\n 1158 | manager->metered = g_network_monitor_get_network_metered (g_network_monitor_get_default ());\n 1159 | }","lines":8,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-filters-manager.c#L1152-1159"},{"ref":"embed/ephy-filters-manager.c:1196-1198","code":" 1196 | manager->update_timeout_id = g_timeout_add_seconds (manager->metered ? ADBLOCK_FILTER_UPDATE_FREQUENCY_METERED : ADBLOCK_FILTER_UPDATE_FREQUENCY,\n 1197 | (GSourceFunc)update_timeout_cb,\n 1198 | manager);","lines":3,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-filters-manager.c#L1196-1198"}]},
{"id":"LOGIC_35","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"open_dialog_cb appends g_file_get_path(file) results to file_array unconditionally; for non-local GFiles g_file_get_path returns NULL, inserting a NULL before the intended terminator and prematurely truncating the (const char* const*) array passed to webkit_file_chooser_request_select_files.","fix":"Skip files whose g_file_get_path returns NULL.","locations":[{"ref":"embed/ephy-web-view.c:202-209","code":" 202 | for (guint i = 0; i < num_items; i++) {\n 203 | g_autoptr (GFile) file = g_list_model_get_item (files, i);\n 204 | \n 205 | g_ptr_array_add (file_array, g_file_get_path (file));\n 206 | }\n 207 | \n 208 | g_ptr_array_add (file_array, NULL);\n 209 | webkit_file_chooser_request_select_files (request, (const char * const *)file_array->pdata);","lines":8,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-web-view.c#L202-209"}]},
{"id":"LOGIC_36","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"ephy_web_overview_model_set_url_thumbnail can store a NULL value (g_strdup(NULL)) when path is NULL; the next call's g_strcmp0(NULL, NULL)==0 dedup short-circuits and the entry is never refreshed. NULL paths should remove the entry.","fix":"If path is NULL/empty, g_hash_table_remove the url instead of inserting a NULL value.","locations":[{"ref":"embed/web-process-extension/ephy-web-overview-model.c:198-215","code":" 198 | void\n 199 | ephy_web_overview_model_set_url_thumbnail (EphyWebOverviewModel *model,\n 200 | const char *url,\n 201 | const char *path,\n 202 | gboolean notify)\n 203 | {\n 204 | const char *thumbnail_path;\n 205 | \n 206 | g_assert (EPHY_IS_WEB_OVERVIEW_MODEL (model));\n 207 | \n 208 | thumbnail_path = g_hash_table_lookup (model->thumbnails, url);\n 209 | if (g_strcmp0 (thumbnail_path, path) == 0)\n 210 | return;\n 211 | \n 212 | g_hash_table_insert (model->thumbnails, g_strdup (url), g_strdup (path));\n 213 | if (notify)\n 214 | ephy_web_overview_model_notify_thumbnail_changed (model, url, path);\n 215 | }","lines":18,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-web-overview-model.c#L198-215"}]},
{"id":"LOGIC_37","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"easy","description":"ephy_sqlite_statement_bind_null() binds to sqlite parameter index 'column' (0-based), whereas every other bind_* function in this file correctly uses 'column + 1' (sqlite parameter indices are 1-based). Callers pass 0-based column indices, so bind_null would target the wrong parameter (and index 0 is invalid for sqlite). Currently latent because bind_null is unused, but it is a bug waiting to bite.","fix":"Use 'column + 1' in sqlite3_bind_null(), consistent with the other binders.","locations":[{"ref":"lib/ephy-sqlite-statement.c:107","code":" 107 | if (sqlite3_bind_null (self->prepared_statement, column) != SQLITE_OK) {","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/ephy-sqlite-statement.c#L107"},{"ref":"lib/ephy-sqlite-statement.c:121","code":" 121 | if (sqlite3_bind_int (self->prepared_statement, column + 1, value ? 1 : 0) != SQLITE_OK) {","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/ephy-sqlite-statement.c#L121"}]},
{"id":"LOGIC_38","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"easy","description":"g_assert(EPHY_HISTORY_RECORD(local)) / g_assert(EPHY_HISTORY_RECORD(remote)) use the cast macro, not the type-check macro EPHY_IS_HISTORY_RECORD. This only asserts non-NULL (and emits a cast warning) rather than verifying the GObject type, defeating the intent of the assertion. Compare the correct EPHY_IS_HISTORY_MANAGER check on the line above.","fix":"Use g_assert(EPHY_IS_HISTORY_RECORD(local)); and g_assert(EPHY_IS_HISTORY_RECORD(remote));","locations":[{"ref":"lib/sync/ephy-history-manager.c:317-318","code":" 317 | g_assert (EPHY_HISTORY_RECORD (local));\n 318 | g_assert (EPHY_HISTORY_RECORD (remote));","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-history-manager.c#L317-318"}]},
{"id":"LOGIC_39","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"easy","description":"hawk_compute_payload_hash asserts content_type is non-NULL, but ephy_sync_crypto_hawk_header_new selects the content type as `options ? options->content_type : \"text/plain\"`; when options is non-NULL but options->content_type is NULL, NULL is passed to hawk_compute_payload_hash, tripping the g_assert and aborting.","fix":"Use `(options && options->content_type) ? options->content_type : \"text/plain\"`.","locations":[{"ref":"lib/sync/ephy-sync-crypto.c:342-346","code":" 342 | if (!hash && payload) {\n 343 | const char *content_type = options ? options->content_type : \"text/plain\";\n 344 | \n 345 | /* Calculate hash for the given payload. */\n 346 | hash = hawk_compute_payload_hash (payload, content_type);","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-crypto.c#L342-346"},{"ref":"lib/sync/ephy-sync-crypto.c:161-172","code":" 161 | static char *\n 162 | hawk_compute_payload_hash (const char *payload,\n 163 | const char *content_type)\n 164 | {\n 165 | guint8 *digest;\n 166 | char *digest_hex;\n 167 | char *content;\n 168 | char *update;\n 169 | char *hash;\n 170 | \n 171 | g_assert (payload);\n 172 | g_assert (content_type);","lines":12,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-crypto.c#L161-172"}]},
{"id":"LOGIC_40","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"easy","description":"get_storage_credentials_cb treats a JSON 'duration' of 0 as an error (if (!api_endpoint || !id || !key || !duration)), and verify_storage_version_cb similarly rejects storageVersion 0 (if (!json_object_get_int_member(json,\"storageVersion\"))). json_object_get_int_member also emits a critical and returns 0 when the member is missing or not an int, so these checks conflate 'absent' with 'legitimately zero' and cannot distinguish them.","fix":"Use json_object_has_member()/json_object_get_member() type checks instead of truthiness of the parsed integer.","locations":[{"ref":"lib/sync/ephy-sync-service.c:767-771","code":" 767 | duration = json_object_get_int_member (json, \"duration\");\n 768 | if (!api_endpoint || !id || !key || !duration) {\n 769 | g_warning (\"JSON object has missing or invalid members\");\n 770 | goto out_error;\n 771 | }","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-service.c#L767-771"},{"ref":"lib/sync/ephy-sync-service.c:2259-2263","code":" 2259 | if (!json_object_get_int_member (json, \"storageVersion\")) {\n 2260 | g_warning (\"JSON object has missing or invalid 'storageVersion' member\");\n 2261 | goto out_error;\n 2262 | }\n 2263 | storage_version = json_object_get_int_member (json, \"storageVersion\");","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-service.c#L2259-2263"}]},
{"id":"LOGIC_41","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"easy","description":"ephy_synchronizable_from_bso rejects a record whenever server_time_modified == 0 (if (!payload || !server_time_modified)). A legitimate 'modified' value of 0 (or a record at the server epoch) would be discarded. Additionally json_object_get_double_member emits a critical and returns 0 if 'modified' is missing or not numeric, so this also conflates absent with zero.","fix":"Check membership/type with json_object_has_member + json_object_get_member rather than treating 0 as missing.","locations":[{"ref":"lib/sync/ephy-synchronizable.c:165-170","code":" 165 | payload = json_object_get_string_member (json, \"payload\");\n 166 | server_time_modified = json_object_get_double_member (json, \"modified\");\n 167 | if (!payload || !server_time_modified) {\n 168 | g_warning (\"JSON object has missing or invalid members\");\n 169 | goto out;\n 170 | }","lines":6,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-synchronizable.c#L165-170"}]},
{"id":"LOGIC_42","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"easy","description":"`(read_bytes % 1) == 0` is always true (any integer modulo 1 is 0), so the grouping condition is meaningless and the branch effectively always runs. The result happens to look correct (a space between each byte) but the modulus is clearly a typo/dead condition and obscures intent.","fix":"Replace with the intended grouping size, or drop the modulus entirely if a space before every byte is desired.","locations":[{"ref":"src/ephy-certificate-dialog.c:81","code":" 81 | if (read_bytes && (read_bytes % 1) == 0)","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-certificate-dialog.c#L81"}]},
{"id":"LOGIC_43","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"download_removed_cb hides the popover \"before removing the last download widget\" by checking row index 2 (`!gtk_list_box_get_row_at_index(..., 2)`), i.e. it hides whenever there are 2 or fewer rows. The comment says it should hide before removing the LAST one (index 0 being the last). With <=2 downloads the popover is dismissed prematurely while another download row still remains.","fix":"Check for the presence of a second row (index 1) to detect that exactly one will remain, or re-evaluate the index to match the intended \"only one left\" condition.","locations":[{"ref":"src/ephy-downloads-popover.c:102-105","code":" 102 | /* Hide the popover before removing the last download widget so it \"crumples\"\n 103 | * more smoothly */\n 104 | if (!gtk_list_box_get_row_at_index (GTK_LIST_BOX (popover->downloads_box), 2))\n 105 | gtk_widget_set_visible (GTK_WIDGET (popover), FALSE);","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-downloads-popover.c#L102-105"}]},
{"id":"LOGIC_44","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"ephy_location_entry_reset() computes offset = strlen(text) - strlen(old_text) using byte lengths, then sets the cursor with gtk_editable_set_position(position + offset). For multi-byte UTF-8 addresses the byte-length delta does not equal the character-position delta GtkEditable expects, so the caret can land at a wrong position after Escape-reset of a URL containing non-ASCII characters.","fix":"Use g_utf8_strlen() for both lengths when computing the offset, consistent with how positions are treated as character offsets elsewhere.","locations":[{"ref":"src/ephy-location-entry.c:1448-1451","code":" 1448 | offset = strlen (text) - strlen (old_text);\n 1449 | position = gtk_editable_get_position (GTK_EDITABLE (self));\n 1450 | ephy_location_entry_do_set_address (EPHY_TITLE_WIDGET (self), text, TRUE);\n 1451 | gtk_editable_set_position (GTK_EDITABLE (self), position + offset);","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-location-entry.c#L1448-1451"}]},
{"id":"LOGIC_45","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"easy","description":"compare_func() computes (gpointer)closed_tab_get_tab_view(iter) - (gpointer)tab_view and returns it as an int. On 64-bit systems the pointer difference is truncated to 32 bits, so two distinct EphyTabView pointers whose low 32 bits coincide would compare as equal (return 0), causing ephy_session_ref_or_create_tab_view_tracker() to reuse the wrong tracker. It is only ever used for equality via g_queue_find_custom.","fix":"Return a 0/1 equality result instead of a truncated pointer difference, e.g. return closed_tab_get_tab_view (iter) != tab_view;","locations":[{"ref":"src/ephy-session.c:184-197","code":" 184 | static int\n 185 | compare_func (ClosedTab *iter,\n 186 | EphyTabView *tab_view)\n 187 | {\n 188 | return (gpointer)closed_tab_get_tab_view (iter) - (gpointer)tab_view;\n 189 | }\n 190 | \n 191 | static TabViewTracker *\n 192 | ephy_session_ref_or_create_tab_view_tracker (EphySession *session,\n 193 | EphyTabView *tab_view)\n 194 | {\n 195 | GList *item = g_queue_find_custom (session->closed_tabs, tab_view, (GCompareFunc)compare_func);\n 196 | return item ? tab_view_tracker_ref (((ClosedTab *)item->data)->tab_view_tracker) : tab_view_tracker_new (tab_view);\n 197 | }","lines":14,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-session.c#L184-197"}]},
{"id":"LOGIC_46","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"In autofill 'Delete all entries', the two combo rows are reset with adw_combo_row_set_selected(...,0), which fires on_country_selected/on_card_selected and WRITES the index-0 values (e.g. first country / first card type and their codes) back into storage instead of clearing them. After a 'Delete All' the country/card-type fields hold the first list item rather than being empty, contradicting the dialog's promise to clear all stored entries.","fix":"Explicitly clear those fields (ephy_autofill_storage_set(EPHY_AUTOFILL_FIELD_COUNTRY, \"\", ...), etc.) or block the combo notify handlers while resetting.","locations":[{"ref":"src/preferences/autofill-view.c:335-354","code":" 335 | static void\n 336 | confirmation_dialog_response_cb (EphyAutoFillView *self)\n 337 | {\n 338 | gtk_editable_set_text (GTK_EDITABLE (self->first_name), \"\");\n 339 | gtk_editable_set_text (GTK_EDITABLE (self->last_name), \"\");\n 340 | gtk_editable_set_text (GTK_EDITABLE (self->full_name), \"\");\n 341 | gtk_editable_set_text (GTK_EDITABLE (self->user_name), \"\");\n 342 | gtk_editable_set_text (GTK_EDITABLE (self->email), \"\");\n 343 | gtk_editable_set_text (GTK_EDITABLE (self->phone), \"\");\n 344 | gtk_editable_set_text (GTK_EDITABLE (self->street), \"\");\n 345 | gtk_editable_set_text (GTK_EDITABLE (self->organization), \"\");\n 346 | gtk_editable_set_text (GTK_EDITABLE (self->postal_code), \"\");\n 347 | gtk_editable_set_text (GTK_EDITABLE (self->state), \"\");\n 348 | gtk_editable_set_text (GTK_EDITABLE (self->city), \"\");\n 349 | gtk_editable_set_text (GTK_EDITABLE (self->card_owner), \"\");\n 350 | gtk_editable_set_text (GTK_EDITABLE (self->card_number), \"\");\n 351 | \n 352 | adw_combo_row_set_selected (ADW_COMBO_ROW (self->country), 0);\n 353 | adw_combo_row_set_selected (ADW_COMBO_ROW (self->card_type), 0);\n 354 | }","lines":20,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/preferences/autofill-view.c#L335-354"}]},
{"id":"LOGIC_47","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"easy","description":"feature_switch_reset_cb sets the reset button sensitivity from the feature's default value: gtk_widget_set_sensitive(button, !enabled) where enabled is webkit_feature_get_default_value. After a reset the value always equals the default, so the button should always become insensitive; for features whose default is FALSE this leaves the reset button erroneously sensitive.","fix":"Set gtk_widget_set_sensitive (button, FALSE); after a reset (or remove the line and rely on the notify::active callback which already does this correctly).","locations":[{"ref":"src/preferences/prefs-features-page.c:144-155","code":" 144 | gboolean enabled = webkit_feature_get_default_value (feature);\n 145 | \n 146 | if (enabled != webkit_settings_get_feature_enabled (settings, feature)) {\n 147 | GtkWidget *parent = gtk_widget_get_ancestor (button, ADW_TYPE_ACTION_ROW);\n 148 | PrefsFeaturesPage *self = EPHY_PREFS_FEATURES_PAGE (gtk_widget_get_ancestor (\n 149 | button,\n 150 | EPHY_TYPE_PREFS_FEATURES_PAGE));\n 151 | \n 152 | GtkWidget *switch_widget = adw_action_row_get_activatable_widget (ADW_ACTION_ROW (parent));\n 153 | webkit_settings_set_feature_enabled (settings, feature, enabled);\n 154 | gtk_switch_set_active (GTK_SWITCH (switch_widget), enabled);\n 155 | gtk_widget_set_sensitive (button, !enabled);","lines":12,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/preferences/prefs-features-page.c#L144-155"}]},
{"id":"LOGIC_48","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"webapp-additional-urls-dialog realize_dialog_cb populates one row per stored URL on every 'realize' signal with no de-dup guard and no cleanup on 'unrealize'. A widget can be unrealized and realized again (re-parenting / navigation), each time appending the full URL list again, producing duplicate rows.","fix":"Populate once (in init/constructed or behind a populated flag), or clear the group at the start of the callback before re-adding.","locations":[{"ref":"src/preferences/webapp-additional-urls-dialog.c:183-201","code":" 183 | realize_dialog_cb (GtkWidget *widget,\n 184 | gpointer user_data)\n 185 | {\n 186 | EphyWebappAdditionalURLsDialog *self = EPHY_WEBAPP_ADDITIONAL_URLS_DIALOG (widget);\n 187 | g_auto (GStrv) urls = NULL;\n 188 | guint i;\n 189 | \n 190 | urls = g_settings_get_strv (EPHY_SETTINGS_WEB_APP, EPHY_PREFS_WEB_APP_ADDITIONAL_URLS);\n 191 | for (i = 0; urls[i]; i++)\n 192 | add_row_internal (self, urls[i]);\n 193 | }\n 194 | \n 195 | static void\n 196 | ephy_webapp_additional_urls_dialog_init (EphyWebappAdditionalURLsDialog *dialog)\n 197 | {\n 198 | gtk_widget_init_template (GTK_WIDGET (dialog));\n 199 | \n 200 | g_signal_connect (GTK_WIDGET (dialog), \"realize\", G_CALLBACK (realize_dialog_cb), NULL);\n 201 | }","lines":19,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/preferences/webapp-additional-urls-dialog.c#L183-201"}]},
{"id":"LOGIC_49","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"easy","description":"handle_activate_result() dereferences terms[0] for the special:load case (launch_uri (terms[0], ...)) without checking that terms is non-empty. A malicious or buggy D-Bus caller can invoke ActivateResult with identifier \"special:load\" and an empty terms array, passing a NULL/garbage pointer to launch_uri -> g_shell_quote, causing a crash. The same unchecked terms[0] indexing is relied upon elsewhere too.","fix":"Validate g_strv_length (terms) >= 1 before indexing terms[0] in the special:load branch and bail out (complete with an empty result / return) otherwise.","locations":[{"ref":"src/search-provider/ephy-search-provider.c:308-313","code":" 308 | if (strcmp (identifier, \"special:search\") == 0)\n 309 | launch_search (self, terms, timestamp);\n 310 | else if (strcmp (identifier, \"special:load\") == 0)\n 311 | launch_uri (terms[0], timestamp);\n 312 | else\n 313 | launch_uri (identifier, timestamp);","lines":6,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/search-provider/ephy-search-provider.c#L308-313"}]},
{"id":"LOGIC_50","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"easy","description":"cookie_compare_func() returns strlen(path_b) - strlen(path_a) as an int. The size_t subtraction can underflow and, when narrowed to int, yields an inconsistent comparator for pathological path lengths (undefined sort ordering).","fix":"Return `(lb > la) - (lb < la)` using size_t lengths, avoiding subtraction.","locations":[{"ref":"src/webextension/api/cookies.c:411-419","code":" 411 | static int\n 412 | cookie_compare_func (SoupCookie *a,\n 413 | SoupCookie *b)\n 414 | {\n 415 | const char *path_a = soup_cookie_get_path (a);\n 416 | const char *path_b = soup_cookie_get_path (b);\n 417 | \n 418 | return strlen (path_b) - strlen (path_a);\n 419 | }","lines":9,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/cookies.c#L411-419"}]},
{"id":"LOGIC_51","type":"logic_error","type_label":"Logic Error","severity":"negligible","difficulty":"easy","description":"ephy_download_get_initiating_web_extension_info uses no-op ternaries `x ? x : NULL`, equivalent to just x.","fix":"Assign the fields directly.","locations":[{"ref":"embed/ephy-download.c:1287-1291","code":" 1287 | if (extension_name_out)\n 1288 | *extension_name_out = download->initiated_by_extension_name ? download->initiated_by_extension_name : NULL;\n 1289 | \n 1290 | if (extension_id_out)\n 1291 | *extension_id_out = download->initiated_by_extension_id ? download->initiated_by_extension_id : NULL;","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-download.c#L1287-1291"}]},
{"id":"CPY_1","type":"copy_paste","type_label":"Copy Paste","severity":"high","difficulty":"easy","description":"webkit_pref_callback_user_javascript() disconnects the JavaScript file monitor signal but then clears the WRONG monitor object: g_clear_object (&user_style_sheet_monitor) instead of &user_javascript_monitor. This leaks the old JS monitor, leaves user_javascript_monitor a stale/destroyed pointer (later reuse condition checks it as non-NULL), and improperly destroys the active stylesheet monitor, breaking user-stylesheet live reload.","fix":"Replace g_clear_object (&user_style_sheet_monitor) with g_clear_object (&user_javascript_monitor).","locations":[{"ref":"embed/ephy-embed-prefs.c:241-244","code":" 241 | if (user_javascript_monitor) {\n 242 | g_signal_handlers_disconnect_by_func (user_javascript_monitor, user_javascript_file_changed, NULL);\n 243 | g_clear_object (&user_style_sheet_monitor);\n 244 | }","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-embed-prefs.c#L241-244"}]},
{"id":"CPY_2","type":"copy_paste","type_label":"Copy Paste","severity":"medium","difficulty":"easy","description":"ephy_autofill_storage_delete_finish() calls secret_password_store_finish() to finish an operation that was started with secret_password_clear(). The matching finish function is secret_password_clear_finish(). Using the wrong finish function on the GAsyncResult is libsecret API misuse and returns/interprets the wrong result for a delete.","fix":"Call secret_password_clear_finish (res, error) instead of secret_password_store_finish().","locations":[{"ref":"lib/autofill/ephy-autofill-storage.c:288-293","code":" 288 | gboolean\n 289 | ephy_autofill_storage_delete_finish (GAsyncResult *res,\n 290 | GError **error)\n 291 | {\n 292 | return secret_password_store_finish (res, error);\n 293 | }","lines":6,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/autofill/ephy-autofill-storage.c#L288-293"}]},
{"id":"CPY_3","type":"copy_paste","type_label":"Copy Paste","severity":"medium","difficulty":"easy","description":"cookies.set() reads the HttpOnly flag from the wrong JSON key \"httpOnline\" (a typo) instead of \"httpOnly\". As a result http_only is always FALSE and an extension can never set an HttpOnly cookie. add_cookie_to_json() correctly emits the key as \"httpOnly\" (line 76), confirming the intended name.","fix":"Change \"httpOnline\" to \"httpOnly\".","locations":[{"ref":"src/webextension/api/cookies.c:294","code":" 294 | http_only = ephy_json_object_get_boolean (details, \"httpOnline\", FALSE);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/cookies.c#L294"}]},
{"id":"CPY_4","type":"copy_paste","type_label":"Copy Paste","severity":"medium","difficulty":"medium","description":"create_passwords_model() iterates over import_passwords_options but guards each entry with `import_options[i].exists` (the wrong array - the bookmarks options array) while calling import_passwords_options[i].exists(). Since import_options has 4 entries and import_passwords_options has 3, this reads the wrong struct's function pointer to decide visibility (e.g. for i=0 it reads import_options[0].exists which is NULL for the HTML row), so the include/skip logic for the passwords model is incorrect.","fix":"Use import_passwords_options[i].exists for the guard: `if (import_passwords_options[i].exists && !import_passwords_options[i].exists ()) continue;`","locations":[{"ref":"src/window-commands.c:746-751","code":" 746 | for (i = G_N_ELEMENTS (import_passwords_options) - 1; i >= 0; i--) {\n 747 | if (import_options[i].exists && !import_passwords_options[i].exists ())\n 748 | continue;\n 749 | \n 750 | gtk_string_list_append (string_list, import_passwords_options[i].name);\n 751 | }","lines":6,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/window-commands.c#L746-751"}]},
{"id":"CPY_5","type":"copy_paste","type_label":"Copy Paste","severity":"low","difficulty":"medium","description":"In gnome_get_country_from_locale() the codeset details are fetched with language_name_get_codeset_details(translation, ...), but the analogous code in gnome_get_language_from_locale() uses 'locale'. Using 'translation' (the optional translation-locale, which may even be NULL) here looks like a copy/paste mistake and yields the wrong codeset suffix.","fix":"Use 'locale' instead of 'translation' for the codeset lookup, matching gnome_get_language_from_locale().","locations":[{"ref":"lib/contrib/gnome-languages.c:1208","code":" 1208 | language_name_get_codeset_details (translation, &langinfo_codeset, &is_utf8);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/contrib/gnome-languages.c#L1208"},{"ref":"lib/contrib/gnome-languages.c:1127","code":" 1127 | language_name_get_codeset_details (locale, &langinfo_codeset, &is_utf8);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/contrib/gnome-languages.c#L1127"}]},
{"id":"CPY_6","type":"copy_paste","type_label":"Copy Paste","severity":"low","difficulty":"easy","description":"EphySearchEngineRow instance struct declares 'AdwExpanderRow parent_instance;' but the type is registered with G_DEFINE_FINAL_TYPE(... ADW_TYPE_ACTION_ROW) and declared with G_DECLARE_FINAL_TYPE(... AdwActionRow). The embedded parent type does not match the real parent type; misleading and layout-fragile.","fix":"Change the field to 'AdwActionRow parent_instance;'.","locations":[{"ref":"src/preferences/ephy-search-engine-row.c:32","code":" 32 | AdwExpanderRow parent_instance;","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/preferences/ephy-search-engine-row.c#L32"},{"ref":"src/preferences/ephy-search-engine-row.c:39","code":" 39 | G_DEFINE_FINAL_TYPE (EphySearchEngineRow, ephy_search_engine_row, ADW_TYPE_ACTION_ROW)","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/preferences/ephy-search-engine-row.c#L39"}]},
{"id":"CPY_7","type":"copy_paste","type_label":"Copy Paste","severity":"low","difficulty":"easy","description":"browserAction handlers report missing-details errors with the wrong namespace 'tabs.' (e.g. 'tabs.setBadgeText(): Missing details') while other errors in the same functions correctly say 'browserAction.'. Misleading diagnostics for extension authors.","fix":"Use the browserAction.* namespace in these error strings.","locations":[{"ref":"src/webextension/api/browseraction.c:46","code":" 46 | g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, \"tabs.setBadgeText(): Missing details\");","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/browseraction.c#L46"},{"ref":"src/webextension/api/browseraction.c:88","code":" 88 | g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, \"tabs.setBadgeBackgroundColor(): Missing details\");","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/browseraction.c#L88"}]},
{"id":"CPY_8","type":"copy_paste","type_label":"Copy Paste","severity":"low","difficulty":"easy","description":"Inconsistent spelling of the cancel interrupt reason: match_error_to_interrupt_reason() matches against \"USER_CANCELED\" (spec spelling, one L) but error_to_interrupt_reason() emits \"USER_CANCELLED\" (two L). An extension that queries/erases by the value Epiphany itself reported will never match it.","fix":"Use a single canonical spelling (\"USER_CANCELED\", matching the WebExtensions InterruptReason spec) in both functions.","locations":[{"ref":"src/webextension/api/downloads.c:417","code":" 417 | if (strcmp (interrupt_reason, \"USER_CANCELED\") == 0)","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/downloads.c#L417"},{"ref":"src/webextension/api/downloads.c:428","code":" 428 | return \"USER_CANCELLED\";","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/downloads.c#L428"}]},
{"id":"ERR_1","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"medium","difficulty":"medium","description":"on_send_message_finish's success path calls g_variant_get_string on webkit_user_message_get_parameters(response) without checking the parameters GVariant is non-NULL or string-typed. A reply with NULL or non-string parameters causes a critical/abort.","fix":"Check the parameters variant is non-NULL and type 's' before g_variant_get_string; reject otherwise.","locations":[{"ref":"embed/web-process-extension/ephy-webextension-common.c:64-82","code":" 64 | response = webkit_web_page_send_message_to_view_finish (page, result, &error);\n 65 | \n 66 | if (error) {\n 67 | ret = jsc_value_function_call (callback_data->reject_callback, G_TYPE_STRING, error->message, G_TYPE_NONE);\n 68 | } else {\n 69 | const char *json = g_variant_get_string (webkit_user_message_get_parameters (response), NULL);\n 70 | g_autoptr (JSCValue) value = NULL;\n 71 | JSCContext *context = jsc_value_get_context (callback_data->resolve_callback);\n 72 | \n 73 | if (strcmp (json, \"\") == 0) {\n 74 | value = jsc_value_new_undefined (context);\n 75 | ret = jsc_value_function_call (callback_data->resolve_callback, JSC_TYPE_VALUE, value, G_TYPE_NONE);\n 76 | } else if (strcmp (webkit_user_message_get_name (response), \"error\") == 0) {\n 77 | ret = jsc_value_function_call (callback_data->reject_callback, G_TYPE_STRING, json, G_TYPE_NONE);\n 78 | } else {\n 79 | value = jsc_value_new_from_json (context, json);\n 80 | ret = jsc_value_function_call (callback_data->resolve_callback, JSC_TYPE_VALUE, value, G_TYPE_NONE);\n 81 | }\n 82 | }","lines":19,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-webextension-common.c#L64-82"}]},
{"id":"ERR_2","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"medium","difficulty":"medium","description":"ephy_bookmarks_import_from_firefox calls get_existing_bookmark(url, tags, manager) TWICE per row when an existing bookmark is found: once for the condition and once for the value prepended. Besides being wasteful (full linear scan of all bookmarks each call), get_existing_bookmark has the side effect of adding the imported tags to the existing bookmark, so it runs the tag-merge side effect twice. The freshly created 'bookmark' is also leaked in the existing-bookmark branch (it was never added to the GSequence and is dropped).","fix":"Call once: EphyBookmark *existing = get_existing_bookmark (url, tags, manager); then g_sequence_prepend (bookmarks, existing ? existing : bookmark); and g_object_unref(bookmark) when existing is used (or only create bookmark when no existing one).","locations":[{"ref":"src/bookmarks/ephy-bookmarks-import.c:360-363","code":" 360 | if (get_existing_bookmark (url, tags, manager))\n 361 | g_sequence_prepend (bookmarks, get_existing_bookmark (url, tags, manager));\n 362 | else\n 363 | g_sequence_prepend (bookmarks, bookmark);","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmarks-import.c#L360-363"}]},
{"id":"ERR_3","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"medium","difficulty":"easy","description":"ephy_web_extension_get_icon() ignores the return of gdk_pixbuf_loader_write_bytes and the possible NULL from gdk_pixbuf_loader_get_pixbuf(); gdk_pixbuf_copy(NULL) then triggers a NULL-pointer assertion. Icon bytes derive from untrusted extension resources, and the close-failure check `!close() && error` lets a failed close fall through.","fix":"Return NULL if write_bytes fails or close returns FALSE, and NULL-check gdk_pixbuf_loader_get_pixbuf before gdk_pixbuf_copy.","locations":[{"ref":"src/webextension/ephy-web-extension.c:208-216","code":" 208 | loader = gdk_pixbuf_loader_new ();\n 209 | gdk_pixbuf_loader_write_bytes (loader, g_bytes_icon_get_bytes (icon), &error);\n 210 | if (error)\n 211 | return NULL;\n 212 | \n 213 | if (!gdk_pixbuf_loader_close (loader, &error) && error)\n 214 | return NULL;\n 215 | \n 216 | return gdk_pixbuf_copy (gdk_pixbuf_loader_get_pixbuf (loader));","lines":9,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension.c#L208-216"}]},
{"id":"ERR_4","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"easy","description":"next_object_cb cancels authentication on error but does not gck_session_logout_async, leaving the just-logged-in PKCS#11 session logged in until the manager is freed.","fix":"Logout the session on the error path, matching process_next_object's empty-objects branch.","locations":[{"ref":"embed/ephy-client-certificate-manager.c:226-243","code":" 226 | next_object_cb (GObject *obj,\n 227 | GAsyncResult *res,\n 228 | gpointer user_data)\n 229 | {\n 230 | EphyClientCertificateManager *self = user_data;\n 231 | GckEnumerator *enm = GCK_ENUMERATOR (obj);\n 232 | g_autoptr (GError) error = NULL;\n 233 | \n 234 | self->objects = gck_enumerator_next_finish (enm, res, &error);\n 235 | if (error) {\n 236 | if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))\n 237 | g_warning (\"Error getting client certificate object: %s\", error->message);\n 238 | cancel_authentication (self);\n 239 | return;\n 240 | }\n 241 | \n 242 | process_next_object (self);\n 243 | }","lines":18,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-client-certificate-manager.c#L226-243"}]},
{"id":"ERR_5","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"easy","description":"download_finished_cb logs g_file_get_uri(file) directly in g_warning; g_file_get_uri returns an allocated string that is never freed.","fix":"g_autofree char *uri = g_file_get_uri(file); use uri in the warning.","locations":[{"ref":"embed/ephy-download.c:1028-1030","code":" 1028 | download->file_monitor = g_file_monitor (file, G_FILE_MONITOR_NONE, NULL, &error);\n 1029 | if (!download->file_monitor)\n 1030 | g_warning (\"Could not add a file monitor for %s, error: %s\", g_file_get_uri (file), error->message);","lines":3,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-download.c#L1028-1030"}]},
{"id":"ERR_6","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"ephy_find_toolbar_selection_async warns with error->message on every failure including normal G_IO_ERROR_CANCELLED at teardown, producing spurious warnings.","fix":"Return silently when g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED).","locations":[{"ref":"embed/ephy-find-toolbar.c:530-534","code":" 530 | value = webkit_web_view_evaluate_javascript_finish (web_view, res, &error);\n 531 | if (!value) {\n 532 | g_warning (\"Error running javascript: %s\", error->message);\n 533 | return;\n 534 | }","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-find-toolbar.c#L530-534"}]},
{"id":"ERR_7","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"ephy_web_view_load_error_page and get_style_sheet call g_resources_lookup_data with a NULL GError and never check the returned GBytes for NULL before g_bytes_get_data/g_bytes_unref. A missing/corrupt compiled-in resource becomes a NULL deref with no diagnostic.","fix":"Pass/check a GError or g_assert the GBytes is non-NULL before dereferencing.","locations":[{"ref":"embed/ephy-web-view.c:2189","code":" 2189 | html_file = g_resources_lookup_data (EPHY_PAGE_TEMPLATE_ERROR, 0, NULL);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-web-view.c#L2189"},{"ref":"embed/ephy-web-view.c:1755-1766","code":" 1755 | static char *\n 1756 | get_style_sheet (void)\n 1757 | {\n 1758 | GBytes *bytes;\n 1759 | char *sheet;\n 1760 | \n 1761 | bytes = g_resources_lookup_data (EPHY_PAGE_TEMPLATE_ERROR_CSS, 0, NULL);\n 1762 | sheet = g_strdup (g_bytes_get_data (bytes, NULL));\n 1763 | g_bytes_unref (bytes);\n 1764 | \n 1765 | return sheet;\n 1766 | }","lines":12,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-web-view.c#L1755-1766"}]},
{"id":"ERR_8","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"default_script_world_window_object_cleared_cb obtains parsed_uri via g_uri_parse but never unrefs it on any return path; guid borrowed via g_uri_get_host is used later without copying.","fix":"Use g_autoptr(GUri) parsed_uri and g_strdup the guid (or use it before scope end).","locations":[{"ref":"embed/web-process-extension/ephy-web-process-extension.c:999-1016","code":" 999 | GUri *parsed_uri;\n 1000 | gsize data_size;\n 1001 | \n 1002 | if (!PAGE_IS_EXTENSION (page))\n 1003 | return;\n 1004 | \n 1005 | parsed_uri = g_uri_parse (webkit_web_page_get_uri (page), G_URI_FLAGS_NON_DNS, &error);\n 1006 | if (!parsed_uri) {\n 1007 | g_warning (\"Failed to parse URI of web page: %s\", error->message);\n 1008 | return;\n 1009 | }\n 1010 | \n 1011 | guid = g_uri_get_host (parsed_uri);\n 1012 | extension_data = g_hash_table_lookup (extension->web_extensions, guid);\n 1013 | if (!extension_data) {\n 1014 | g_warning (\"Failed to find extension by guid: %s\", guid);\n 1015 | return;\n 1016 | }","lines":18,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-web-process-extension.c#L999-1016"}]},
{"id":"ERR_9","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"In ephy_sqlite_connection_open() the three PRAGMA executions in the non-memory branch all pass the same 'error' out-param in sequence. If the first one fails it sets *error; the next ephy_sqlite_connection_execute() -> ephy_sqlite_connection_get_error() then does '*error = g_error_new_literal(...)' over the already-set GError, leaking the previous GError and triggering a 'GError set over the top of a previous GError' GLib critical.","fix":"Check the return value after each execute and bail out early, or use separate/local GError variables per statement.","locations":[{"ref":"lib/ephy-sqlite-connection.c:171-175","code":" 171 | } else {\n 172 | ephy_sqlite_connection_execute (self, \"PRAGMA main.journal_mode=WAL\", error);\n 173 | ephy_sqlite_connection_execute (self, \"PRAGMA main.synchronous=NORMAL\", error);\n 174 | ephy_sqlite_connection_execute (self, \"PRAGMA main.cache_size=10000\", error);\n 175 | }","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/ephy-sqlite-connection.c#L171-175"}]},
{"id":"ERR_10","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"Several response bodies obtained via soup_session_send_and_read are passed to NUL-terminated-string APIs (json_from_string, g_ascii_strtod, json_parser_load_from_data with length -1) without guaranteeing NUL termination. soup_session_send_and_read_finish returns a GBytes whose data is not guaranteed to be NUL-terminated, so these reads can run off the end of the buffer. upload_synchronizable_cb's ceil(g_ascii_strtod(g_bytes_get_data(...), NULL)) is the clearest case.","fix":"Pass the explicit byte length to the JSON parser, and for strtod copy the bytes into a NUL-terminated buffer (or append a trailing NUL when storing the GBytes).","locations":[{"ref":"lib/sync/ephy-sync-service.c:717-726","code":" 717 | static JsonNode *\n 718 | json_from_response_body (GBytes *bytes,\n 719 | GError **error)\n 720 | {\n 721 | gconstpointer data = g_bytes_get_data (bytes, NULL);\n 722 | if (data)\n 723 | return json_from_string (data, error);\n 724 | g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, _(\"Response body is empty, do you need to install glib-networking?\"));\n 725 | return NULL;\n 726 | }","lines":10,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-service.c#L717-726"},{"ref":"lib/sync/ephy-sync-service.c:1093","code":" 1093 | time_modified = ceil (g_ascii_strtod (g_bytes_get_data (response_body, NULL), NULL));","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-service.c#L1093"},{"ref":"lib/sync/ephy-sync-service.c:2234","code":" 2234 | json_parser_load_from_data (parser, g_bytes_get_data (response_body, NULL), -1, &error);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-service.c#L2234"}]},
{"id":"ERR_11","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"sync_oauth_token_exchange_cb() creates a throwaway SoupSession with soup_session_new() in sync_message_from_fxa_content_cb (the 'fxaccounts:oauth_login' branch) and immediately g_object_unref()s it after starting the async send. While soup keeps the session alive for the in-flight request, the dialog (sync_dialog) is passed as user_data without taking a ref; if the dialog is closed/finalized before the token exchange completes, the callback dereferences a freed EphyFirefoxSyncDialog (EPHY_FIREFOX_SYNC_DIALOG (user_data)).","fix":"Pass a GCancellable owned by the dialog (cancelled in finalize) to soup_session_send_and_read_async, or g_object_ref the dialog for the duration of the request and unref in the callback.","locations":[{"ref":"src/ephy-firefox-sync-dialog.c:721-732","code":" 721 | msg = soup_message_new (SOUP_METHOD_POST, url);\n 722 | bytes = g_bytes_new_take (request_body, strlen (request_body));\n 723 | soup_message_set_request_body_from_bytes (msg, content_type, bytes);\n 724 | request_headers = soup_message_get_request_headers (msg);\n 725 | soup_message_headers_append (request_headers, \"content-type\", content_type);\n 726 | \n 727 | soup_session = soup_session_new ();\n 728 | soup_session_send_and_read_async (soup_session, msg, G_PRIORITY_DEFAULT, NULL,\n 729 | (GAsyncReadyCallback)sync_oauth_token_exchange_cb,\n 730 | sync_dialog);\n 731 | g_object_unref (soup_session);\n 732 | g_free (url);","lines":12,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-firefox-sync-dialog.c#L721-732"},{"ref":"src/ephy-firefox-sync-dialog.c:467-592","code":" 467 | static void\n 468 | sync_oauth_token_exchange_cb (SoupSession *session,\n 469 | GAsyncResult *result,\n 470 | gpointer user_data)\n 471 | {\n 472 | EphyFirefoxSyncDialog *sync_dialog = EPHY_FIREFOX_SYNC_DIALOG (user_data);\n 473 | g_autoptr (GBytes) bytes = NULL;\n 474 | g_autoptr (GError) error = NULL;\n 475 | SoupMessage *msg;\n 476 | g_autoptr (JsonNode) node = NULL;\n 477 | JsonObject *json;\n 478 | g_autoptr (JsonNode) keys_node = NULL;\n 479 | JsonObject *keys_json;\n 480 | JsonObject *oldsync_key;\n 481 | const char *access_token;\n 482 | const char *refresh_token;\n 483 | const char *keys_jwe;\n 484 | const char *kid;\n 485 | const char *k_b64;\n 486 | g_autofree char *decrypted_keys = NULL;\n 487 | g_autofree guint8 *sync_key = NULL;\n 488 | gsize sync_key_len;\n 489 | g_autofree char *sync_key_hex = NULL;\n 490 | guint status_code;\n 491 | gconstpointer data;\n 492 | \n 493 | bytes = soup_session_send_and_read_finish (session, result, &error);\n 494 | if (!bytes) {\n 495 | g_warning (\"Failed to exchange OAuth code: %s\", error->message);\n 496 | goto out_error;\n 497 | }\n 498 | \n 499 | msg = soup_session_get_async_result_message (session, result);\n 500 | status_code = soup_message_get_status (msg);\n 501 | if (status_code != 200) {\n 502 | data = g_bytes_get_data (bytes, NULL);\n 503 | g_warning (\"Token exchange failed. Status: %u, response: %s\",\n 504 | status_code, data ? (const char *)data : \"(null)\");\n 505 | goto out_error;\n 506 | }\n 507 | \n 508 | data = g_bytes_get_data (bytes, NULL);\n 509 | if (!data) {\n 510 | g_warning (\"Empty response from token exchange\");\n 511 | goto out_error;\n 512 | }\n 513 | \n 514 | node = json_from_string (data, &error);\n 515 | if (error) {\n 516 | g_warning (\"Token response is not valid JSON: %s\", error->message);\n 517 | goto out_error;\n 518 | }\n 519 | json = json_node_get_object (node);\n 520 | if (!json) {\n 521 | g_warning (\"Token response is not a JSON object\");\n 522 | goto out_error;\n 523 | }\n 524 | \n 525 | access_token = json_object_get_string_member (json, \"access_token\");\n 526 | refresh_token = json_object_get_string_member (json, \"refresh_token\");\n 527 | keys_jwe = json_object_get_string_member (json, \"keys_jwe\");\n 528 | if (!access_token || !keys_jwe) {\n 529 | g_warning (\"Token response missing access_token or keys_jwe\");\n 530 | goto out_error;\n 531 | }\n 532 | if (!refresh_token)\n 533 | refresh_token = \"\";\n 534 | \n 535 | /* Decrypt the keys_jwe to get the scoped sync key. */\n 536 | decrypted_keys = ephy_sync_crypto_decrypt_jwe (keys_jwe, sync_dialog->ecdh_key_pair);\n 537 | if (!decrypted_keys) {\n 538 | g_warning (\"Failed to decrypt keys_jwe\");\n 539 | goto out_error;\n 540 | }\n 541 | \n 542 | keys_node = json_from_string (decrypted_keys, &error);\n 543 | if (error || !keys_node) {\n 544 | g_warning (\"Decrypted keys is not valid JSON\");\n 545 | goto out_error;\n 546 | }\n 547 | keys_json = json_node_get_object (keys_node);\n 548 | if (!keys_json) {\n 549 | g_warning (\"Decrypted keys is not a JSON object\");\n 550 | goto out_error;\n 551 | }\n 552 | oldsync_key = json_object_get_object_member (keys_json, FXA_OLDSYNC_SCOPE);\n 553 | if (!oldsync_key) {\n 554 | g_warning (\"Decrypted keys missing oldsync scope\");\n 555 | goto out_error;\n 556 | }\n 557 | \n 558 | kid = json_object_get_string_member (oldsync_key, \"kid\");\n 559 | k_b64 = json_object_get_string_member (oldsync_key, \"k\");\n 560 | if (!kid || !k_b64) {\n 561 | g_warning (\"Scoped key missing kid or k\");\n 562 | goto out_error;\n 563 | }\n 564 | \n 565 | /* Decode the scoped key (base64url) and convert to hex for storage. */\n 566 | sync_key = ephy_sync_utils_base64_urlsafe_decode (k_b64, &sync_key_len, TRUE);\n 567 | if (!sync_key || (sync_key_len != 32 && sync_key_len != 64)) {\n 568 | g_warning (\"Invalid scoped key length: %\" G_GSIZE_FORMAT, sync_key_len);\n 569 | goto out_error;\n 570 | }\n 571 | sync_key_hex = ephy_sync_utils_encode_hex (sync_key, sync_key_len);\n 572 | \n 573 | /* Sign in with the OAuth tokens and scoped key. */\n 574 | ephy_sync_service_sign_in (ephy_shell_get_sync_service (ephy_shell_get_default ()),\n 575 | sync_dialog->sign_in_email,\n 576 | sync_dialog->sign_in_uid,\n 577 | access_token, refresh_token,\n 578 | sync_key_hex, kid);\n 579 | \n 580 | goto out_cleanup;\n 581 | \n 582 | out_error:\n 583 | sync_sign_in_details_show (sync_dialog, _(\"Something went wrong, please try again later.\"));\n 584 | \n 585 | out_cleanup:\n 586 | /* Clean up ephemeral OAuth state. */\n 587 | g_clear_pointer (&sync_dialog->pkce_challenge, ephy_sync_crypto_pkce_challenge_free);\n 588 | g_clear_pointer (&sync_dialog->ecdh_key_pair, ephy_sync_crypto_ecdh_key_pair_free);\n 589 | g_clear_pointer (&sync_dialog->oauth_state, g_free);\n 590 | g_clear_pointer (&sync_dialog->sign_in_email, g_free);\n 591 | g_clear_pointer (&sync_dialog->sign_in_uid, g_free);\n 592 | }","lines":126,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-firefox-sync-dialog.c#L467-592"}]},
{"id":"ERR_12","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"complete_request() returns suggestion results to D-Bus even when the underlying suggestion-model query was cancelled. handle_get_result_metas and handle_activate_result call g_cancellable_cancel() on the shared self->cancellable, which causes ephy_suggestion_model_query_finish to fail; on_model_updated then ignores the failure (just g_warning) and still returns the special:search entry plus whatever stale model contents exist. Combined with a single shared cancellable reused across all concurrent requests, an in-flight initial/subsearch request can be silently corrupted by a later metas/activate call on the same object.","fix":"Use a per-request GCancellable instead of one shared self->cancellable, and in on_model_updated propagate a cancellation/error via g_task_return_error rather than fabricating a result set when the query did not complete successfully.","locations":[{"ref":"src/search-provider/ephy-search-provider.c:60-93","code":" 60 | static void\n 61 | on_model_updated (GObject *source_object,\n 62 | GAsyncResult *result,\n 63 | GTask *task)\n 64 | {\n 65 | EphySearchProvider *self = g_task_get_source_object (task);\n 66 | EphySuggestion *suggestion;\n 67 | GPtrArray *results;\n 68 | const char *search_string;\n 69 | guint n_items;\n 70 | GError *error = NULL;\n 71 | results = g_ptr_array_new ();\n 72 | \n 73 | if (ephy_suggestion_model_query_finish (self->model,\n 74 | result,\n 75 | &error)) {\n 76 | n_items = g_list_model_get_n_items (G_LIST_MODEL (self->model));\n 77 | for (guint i = 0; i < n_items; i++) {\n 78 | suggestion = g_list_model_get_item (G_LIST_MODEL (self->model), i);\n 79 | g_ptr_array_add (results, g_strdup (ephy_suggestion_get_uri (suggestion)));\n 80 | }\n 81 | } else {\n 82 | g_warning (\"Failed to query suggestion model: %s\", error->message);\n 83 | g_error_free (error);\n 84 | }\n 85 | \n 86 | search_string = g_task_get_task_data (task);\n 87 | g_ptr_array_add (results, g_strdup_printf (\"special:search:%s\", search_string));\n 88 | g_ptr_array_add (results, NULL);\n 89 | \n 90 | g_task_return_pointer (task,\n 91 | g_ptr_array_free (results, FALSE),\n 92 | (GDestroyNotify)g_strfreev);\n 93 | }","lines":34,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/search-provider/ephy-search-provider.c#L60-93"},{"ref":"src/search-provider/ephy-search-provider.c:206-207","code":" 206 | g_application_hold (G_APPLICATION (self));\n 207 | g_cancellable_cancel (self->cancellable);","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/search-provider/ephy-search-provider.c#L206-207"}]},
{"id":"ERR_13","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"In favicon_loaded_cb, when webkit_favicon_database_get_favicon_finish() returns NULL for a non-cancellation reason (e.g. favicon simply not found), the code still calls ephy_favicon_get_from_texture_scaled(texture=NULL, ...). Only the G_IO_ERROR_CANCELLED case is handled explicitly. Relying on the helper to tolerate a NULL texture is fragile and the error object is otherwise ignored.","fix":"Handle texture == NULL explicitly (skip scaling and go straight to the fallback icon) rather than passing NULL into ephy_favicon_get_from_texture_scaled().","locations":[{"ref":"src/synced-tabs-dialog.c:106-118","code":" 106 | texture = webkit_favicon_database_get_favicon_finish (database, result, &error);\n 107 | if (!texture && g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {\n 108 | favicon_async_data_free (data);\n 109 | return;\n 110 | }\n 111 | \n 112 | favicon = ephy_favicon_get_from_texture_scaled (texture, FAVICON_SIZE, FAVICON_SIZE);\n 113 | if (!favicon) {\n 114 | const char *icon_name = ephy_get_fallback_favicon_name (data->url, EPHY_FAVICON_TYPE_SHOW_MISSING_PLACEHOLDER);\n 115 | if (!icon_name)\n 116 | icon_name = \"adw-tab-icon-missing-symbolic\";\n 117 | favicon = g_themed_icon_new (icon_name);\n 118 | }","lines":13,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/synced-tabs-dialog.c#L106-118"}]},
{"id":"ERR_14","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"cookies.set() passes an extension-supplied expirationDate straight to g_date_time_new_from_unix_local() without range validation. For out-of-range values that function returns NULL, and soup_cookie_set_expires(cookie, NULL) is then called, silently clearing the expiry instead of reporting an error.","fix":"Check the GDateTime for NULL after g_date_time_new_from_unix_local and return WEB_EXTENSION_ERROR_INVALID_ARGUMENT instead of calling soup_cookie_set_expires with NULL.","locations":[{"ref":"src/webextension/api/cookies.c:320-323","code":" 320 | if (expiration != -1) {\n 321 | expires_date = g_date_time_new_from_unix_local (expiration);\n 322 | soup_cookie_set_expires (new_cookie, expires_date);\n 323 | }","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/cookies.c#L320-323"}]},
{"id":"ERR_15","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"runtime.sendMessage(): a present, non-empty args[1] is treated as an extensionId-style call and rejected with NOT_IMPLEMENTED. Per the WebExtensions argument-parsing rules args[1] is the options object (e.g. {includeTlsChannelId}); only the 3-arg form carries an extensionId. This rejects legitimate two-arg sendMessage(message, options) calls with a misleading error.","fix":"Treat a present args[1] as an options object (ignore unsupported fields); only the 3-arg form indicates an extensionId.","locations":[{"ref":"src/webextension/api/runtime.c:125-131","code":" 125 | last_arg = ephy_json_array_get_element (args, 1);\n 126 | if (!last_arg || json_node_is_null (last_arg) || is_empty_object (last_arg))\n 127 | message = ephy_json_array_get_element (args, 0);\n 128 | else {\n 129 | g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_NOT_IMPLEMENTED, \"extensionId is not supported\");\n 130 | return;\n 131 | }","lines":7,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/runtime.c#L125-131"}]},
{"id":"ERR_16","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"tabs_handler_execute_script() passes the untrusted code to webkit_web_view_evaluate_javascript without checking that target_web_view is an extension/privileged view; it relies only on has_host_or_active_permission. Acceptable, but note on_execute_script_ready returns jsc_value_to_json(value, 0) which may be NULL (e.g. for undefined/unsupported values) and is returned via g_task_return_pointer with g_free - returning NULL is fine, but a NULL here is indistinguishable from an error to the caller.","fix":"Document/normalize the NULL-result case, returning an explicit JSON 'null' string when jsc_value_to_json yields NULL.","locations":[{"ref":"src/webextension/api/tabs.c:391-409","code":" 391 | on_execute_script_ready (GObject *source,\n 392 | GAsyncResult *result,\n 393 | gpointer user_data)\n 394 | {\n 395 | g_autoptr (JSCValue) value = NULL;\n 396 | g_autoptr (GError) error = NULL;\n 397 | GTask *task = user_data;\n 398 | \n 399 | value = webkit_web_view_evaluate_javascript_finish (WEBKIT_WEB_VIEW (source),\n 400 | result,\n 401 | &error);\n 402 | \n 403 | if (error) {\n 404 | g_task_return_error (task, g_steal_pointer (&error));\n 405 | return;\n 406 | }\n 407 | \n 408 | g_task_return_pointer (task, jsc_value_to_json (value, 0), g_free);\n 409 | }","lines":19,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/tabs.c#L391-409"}]},
{"id":"ERR_17","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"on_web_extension_loaded() and on_extension_decompressed() log error->message after a failed load/query without verifying error is non-NULL. ephy_web_extension_load_finished may, on some failure paths, return NULL without setting error; the g_warning then dereferences NULL.","fix":"Guard with `error ? error->message : \"unknown error\"` (or assert error is set on the NULL path).","locations":[{"ref":"src/webextension/ephy-web-extension-manager.c:202-204","code":" 202 | if (!web_extension) {\n 203 | g_warning (\"Failed to load extension %s: %s\", g_file_peek_path (target), error->message);\n 204 | return;","lines":3,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension-manager.c#L202-204"},{"ref":"src/webextension/ephy-web-extension-manager.c:495-497","code":" 495 | if (!file_info) {\n 496 | g_warning (\"Failed to query file info: %s\", error->message);\n 497 | return;","lines":3,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension-manager.c#L495-497"}]},
{"id":"ERR_18","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"easy","description":"ephy_web_extension_load_pixbuf() logs error->message unconditionally when gdk_pixbuf_new_from_stream_at_scale returns NULL; if error is NULL this dereferences NULL. The data is untrusted.","fix":"Use `error ? error->message : \"unknown error\"`.","locations":[{"ref":"src/webextension/ephy-web-extension.c:1265-1269","code":" 1265 | pixbuf = gdk_pixbuf_new_from_stream_at_scale (stream, size, size, TRUE, NULL, &error);\n 1266 | if (!pixbuf) {\n 1267 | g_warning (\"Could not load web extension icon: %s\", error->message);\n 1268 | return NULL;\n 1269 | }","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension.c#L1265-1269"}]},
{"id":"ERR_19","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"window_cmd_show_about(): g_resources_lookup_data() result `bytes` is used with g_bytes_get_data (bytes, NULL) without a NULL check (crash if the resource is missing). Additionally, in the error branch where g_key_file_load_from_data fails, the function returns without g_bytes_unref (bytes), leaking the bytes; the unref only happens on the success path at line 1177.","fix":"NULL-check bytes after lookup, and move g_bytes_unref (bytes) so it runs on all paths (or use g_autoptr (GBytes)).","locations":[{"ref":"src/window-commands.c:1170-1178","code":" 1170 | key_file = g_key_file_new ();\n 1171 | bytes = g_resources_lookup_data (\"/org/gnome/epiphany/about.ini\", 0, NULL);\n 1172 | if (!g_key_file_load_from_data (key_file, g_bytes_get_data (bytes, NULL), -1, 0, &error)) {\n 1173 | g_warning (\"Couldn't load about data: %s\\n\", error->message);\n 1174 | g_error_free (error);\n 1175 | g_key_file_free (key_file);\n 1176 | return;\n 1177 | }\n 1178 | g_bytes_unref (bytes);","lines":9,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/window-commands.c#L1170-1178"}]},
{"id":"API_1","type":"api_misuse","type_label":"Api Misuse","severity":"high","difficulty":"medium","description":"open_download_confirmation_dialog connects 'clicked' on the folder button to filename_suggested_button_cb and 'response' on the alert dialog to filename_suggested_dialog_cb, both with the SAME data. filename_suggested_dialog_cb frees data on dialog response. If the alert is responded/dismissed while the GtkFileDialog opened by the button is still pending, the pending file-dialog callback uses freed data (use-after-free). The file dialog uses download->cancellable, only cancelled at EphyDownload dispose, not on alert response.","fix":"Refcount/share the data so it outlives both callbacks, or cancel the pending file dialog in filename_suggested_dialog_cb before freeing data.","locations":[{"ref":"embed/ephy-download.c:863-866","code":" 863 | g_signal_connect (button, \"clicked\",\n 864 | G_CALLBACK (filename_suggested_button_cb), data);\n 865 | g_signal_connect (dialog, \"response\",\n 866 | G_CALLBACK (filename_suggested_dialog_cb), data);","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-download.c#L863-866"}]},
{"id":"API_2","type":"api_misuse","type_label":"Api Misuse","severity":"high","difficulty":"easy","description":"js_getuilanguage() writes a NUL into the buffer returned by setlocale(). That buffer is libc-owned static storage and must be treated as read-only; mutating it is undefined behavior and can corrupt locale state used by every later setlocale/gettext call. Also, if the locale string is shorter than 3 bytes (e.g. \"C\"), locale[2] is an out-of-bounds write.","fix":"Duplicate first: g_autofree char *locale = g_strdup (setlocale (LC_MESSAGES, NULL)); bounds-check length before writing locale[2]='\\0'; return g_strdup of the truncated copy.","locations":[{"ref":"embed/web-process-extension/ephy-webextension-common.c:137-149","code":" 137 | static char *\n 138 | js_getuilanguage (void)\n 139 | {\n 140 | char *locale = setlocale (LC_MESSAGES, NULL);\n 141 | \n 142 | if (locale) {\n 143 | locale[2] = '\\0';\n 144 | \n 145 | return g_strdup (locale);\n 146 | }\n 147 | \n 148 | return g_strdup (\"en\");\n 149 | }","lines":13,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-webextension-common.c#L137-149"}]},
{"id":"API_3","type":"api_misuse","type_label":"Api Misuse","severity":"high","difficulty":"medium","description":"g_mkdir_with_parents() is called with mode 488 (decimal), which is octal 0750. The literal is clearly intended to be an octal permission constant (e.g. 0700/0755). 488 decimal == 0750 octal happens to be a plausible mode by luck, but it is a typo: any reader/maintainer changing it will get wildly wrong permissions, and it is inconsistent with the 0700/0600 octal literals used elsewhere in the same files.","fix":"Replace the 488 literals with the intended octal mode, e.g. 0700 (these are private profile/config dirs).","locations":[{"ref":"lib/ephy-file-helpers.c:562","code":" 562 | if (g_mkdir_with_parents (dir, 488) == 0) {","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/ephy-file-helpers.c#L562"},{"ref":"lib/ephy-web-app-utils.c:440","code":" 440 | if (g_mkdir_with_parents (profile_dir, 488) == -1) {","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/ephy-web-app-utils.c#L440"},{"ref":"lib/ephy-web-app-utils.c:480","code":" 480 | if (g_mkdir (profile_dir, 488) == -1) {","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/ephy-web-app-utils.c#L480"}]},
{"id":"API_4","type":"api_misuse","type_label":"Api Misuse","severity":"high","difficulty":"medium","description":"bookmarks_order and tags_order GSequences are created with g_free as their destroy func, but their elements are GVariant* (added in ephy_bookmarks_manager_add_to_bookmarks_order via g_variant_new, in ephy_bookmarks_manager_tags_order_add_tag/add_tag_variant). When the sequence removes/frees an element it calls g_free() on a GVariant instead of g_variant_unref(), corrupting GVariant refcounting (the variant data is leaked and the GVariant box freed with the wrong allocator). Also the imported variants stored via add_tag_variant are floating refs from gvdb that are never sunk.","fix":"Create both order sequences with (GDestroyNotify)g_variant_unref, and g_variant_ref_sink variants before storing (line 634 and 740) so refcounting is correct.","locations":[{"ref":"src/bookmarks/ephy-bookmarks-manager.c:263-264","code":" 263 | self->bookmarks_order = g_sequence_new (g_free);\n 264 | self->tags_order = g_sequence_new (g_free);","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmarks-manager.c#L263-264"},{"ref":"src/bookmarks/ephy-bookmarks-manager.c:660-667","code":" 660 | GVariant *variant;\n 661 | \n 662 | g_assert (EPHY_IS_BOOKMARKS_MANAGER (self));\n 663 | \n 664 | variant = g_variant_new (\"(ssi)\", type, item, index);\n 665 | g_sequence_insert_sorted (self->bookmarks_order, variant,\n 666 | (GCompareDataFunc)sort_bookmarks_order, NULL);\n 667 | }","lines":8,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmarks-manager.c#L660-667"},{"ref":"src/bookmarks/ephy-bookmarks-manager.c:770-781","code":" 770 | variant = g_variant_builder_end (&builder);\n 771 | g_sequence_append (self->tags_order, variant);\n 772 | \n 773 | g_sequence_free (urls);\n 774 | }\n 775 | \n 776 | void\n 777 | ephy_bookmarks_manager_tags_order_add_tag_variant (EphyBookmarksManager *self,\n 778 | GVariant *variant)\n 779 | {\n 780 | g_sequence_append (self->tags_order, variant);\n 781 | }","lines":12,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmarks-manager.c#L770-781"}]},
{"id":"API_5","type":"api_misuse","type_label":"Api Misuse","severity":"high","difficulty":"easy","description":"ephy_bookmarks_manager_clear_bookmarks_order calls g_free() on self->bookmarks_order, which is a GSequence* (not a g_malloc'd block). This frees the opaque GSequence handle with the wrong allocator and leaks every element/node the sequence holds. It must use g_sequence_free.","fix":"Replace g_free (self->bookmarks_order); with g_sequence_free (self->bookmarks_order);","locations":[{"ref":"src/bookmarks/ephy-bookmarks-manager.c:674-675","code":" 674 | g_free (self->bookmarks_order);\n 675 | self->bookmarks_order = g_sequence_new (g_free);","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmarks-manager.c#L674-675"}]},
{"id":"API_6","type":"api_misuse","type_label":"Api Misuse","severity":"high","difficulty":"easy","description":"In prefs-features-page constructed, the 'notify::active' handler on the switch is connected with g_signal_connect_data passing the GtkButton 'reset' as user_data but (GClosureNotify)webkit_feature_unref as the destroy notify. When the closure is destroyed GLib will invoke webkit_feature_unref(reset), i.e. run webkit_feature_unref on a GtkButton pointer - type confusion / invalid unref leading to corruption or crash at widget teardown. (The reset button's own handler just above correctly pairs webkit_feature_ref(feature) with webkit_feature_unref.)","fix":"Connect without a destroy notify: g_signal_connect (switch_widget, \"notify::active\", G_CALLBACK (feature_switch_notify_active_cb), reset); (reset is owned by the row, no extra ref/notify needed).","locations":[{"ref":"src/preferences/prefs-features-page.c:343-348","code":" 343 | g_signal_connect_data (switch_widget,\n 344 | \"notify::active\",\n 345 | G_CALLBACK (feature_switch_notify_active_cb),\n 346 | reset,\n 347 | (GClosureNotify)webkit_feature_unref,\n 348 | G_CONNECT_DEFAULT);","lines":6,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/preferences/prefs-features-page.c#L343-348"}]},
{"id":"API_7","type":"api_misuse","type_label":"Api Misuse","severity":"high","difficulty":"medium","description":"regex_matches() builds JS source by printf-substituting fully untrusted strings (the extension-supplied filenameRegex AND the download URL/filename) into a single-quoted JS string literal, then evaluates it. A value containing a single quote or backslash breaks out of the literal, causing silent no-match (exception swallowed by jsc_value_to_boolean) or unintended JS execution in the throwaway context. The in-file FIXME acknowledges this is unsafe.","fix":"Build the RegExp via jsc_value_constructor_call() with the pattern passed as a JSCValue argument and invoke .test() with the subject passed as a JSCValue argument, so no string interpolation/escaping is involved.","locations":[{"ref":"src/webextension/api/downloads.c:325-338","code":" 325 | regex_matches (JSCContext *context,\n 326 | const char *regex,\n 327 | const char *string)\n 328 | {\n 329 | /* WebExtensions can include arbitrary regex; To match expectations we need to run this against\n 330 | * the JavaScript implementation of regex rather than PCREs.\n 331 | * Note that this is absolutely untrusted code, however @context is private to this single API call\n 332 | * so they cannot actually do anything except make this match succeed or fail. */\n 333 | /* FIXME: Maybe this can use `jsc_value_constructor_call()` and `jsc_context_evaluate_in_object()` instead of\n 334 | * printf to avoid quotes potentially conflicting. */\n 335 | g_autofree char *code = g_strdup_printf (\"let re = new RegExp('%s'); re.test('%s');\", regex, string);\n 336 | g_autoptr (JSCValue) ret = jsc_context_evaluate (context, code, -1);\n 337 | return jsc_value_to_boolean (ret);\n 338 | }","lines":14,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/downloads.c#L325-338"}]},
{"id":"API_8","type":"api_misuse","type_label":"Api Misuse","severity":"medium","difficulty":"medium","description":"ephy_embed_shell_set_print_settings refs priv->print_settings to (settings ? settings : gtk_print_settings_new()), but then writes the ORIGINAL `settings` parameter to file with gtk_print_settings_to_file(settings, ...). When settings is NULL this passes NULL to gtk_print_settings_to_file (invalid), and the freshly created empty settings are never persisted.","fix":"Write priv->print_settings to file: gtk_print_settings_to_file (priv->print_settings, path, NULL).","locations":[{"ref":"embed/ephy-embed-shell.c:1233-1242","code":" 1233 | if (settings)\n 1234 | g_object_ref (settings);\n 1235 | \n 1236 | if (priv->print_settings)\n 1237 | g_object_unref (priv->print_settings);\n 1238 | \n 1239 | priv->print_settings = settings ? settings : gtk_print_settings_new ();\n 1240 | \n 1241 | path = g_build_filename (ephy_profile_dir (), PRINT_SETTINGS_FILENAME, NULL);\n 1242 | gtk_print_settings_to_file (settings, path, NULL);","lines":10,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-embed-shell.c#L1233-1242"}]},
{"id":"API_9","type":"api_misuse","type_label":"Api Misuse","severity":"medium","difficulty":"medium","description":"ephy_embed_detach_notification_container takes a g_object_ref() on the singleton container and passes it to gtk_overlay_remove_overlay() but never releases that ref, leaking one ref per detach/attach cycle.","fix":"Ref, remove, then g_object_unref after gtk_overlay_remove_overlay returns.","locations":[{"ref":"embed/ephy-embed.c:1026-1032","code":" 1026 | if (gtk_widget_get_parent (GTK_WIDGET (container)) == embed->overlay) {\n 1027 | /* Since the overlay container will own the one and only reference to the\n 1028 | * notification widget, removing it from the container will destroy the\n 1029 | * singleton. To prevent this, add a reference to it before removing it\n 1030 | * from the container. */\n 1031 | gtk_overlay_remove_overlay (GTK_OVERLAY (embed->overlay), g_object_ref (GTK_WIDGET (container)));\n 1032 | }","lines":7,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-embed.c#L1026-1032"}]},
{"id":"API_10","type":"api_misuse","type_label":"Api Misuse","severity":"medium","difficulty":"medium","description":"ephy_file_monitor_update_location assigns the result of g_file_monitor_directory/g_file_monitor_file (NULL GError) to file_monitor->monitor without a NULL check, then g_signal_connect on it. If monitoring fails, monitor is NULL and the connect raises a GLib critical and is lost.","fix":"Check the monitor is non-NULL before connecting 'changed', and handle failure (pass/log a GError).","locations":[{"ref":"embed/ephy-file-monitor.c:200-214","code":" 200 | if (file_type == G_FILE_TYPE_DIRECTORY) {\n 201 | file_monitor->monitor = g_file_monitor_directory (file, 0, NULL, NULL);\n 202 | g_signal_connect (file_monitor->monitor, \"changed\",\n 203 | G_CALLBACK (ephy_file_monitor_changed_cb),\n 204 | file_monitor);\n 205 | file_monitor->monitor_directory = TRUE;\n 206 | LOG (\"Installed monitor for directory '%s'\", url);\n 207 | } else if (file_type == G_FILE_TYPE_REGULAR) {\n 208 | file_monitor->monitor = g_file_monitor_file (file, 0, NULL, NULL);\n 209 | g_signal_connect (file_monitor->monitor, \"changed\",\n 210 | G_CALLBACK (ephy_file_monitor_changed_cb),\n 211 | file_monitor);\n 212 | file_monitor->monitor_directory = FALSE;\n 213 | LOG (\"Installed monitor for file '%s'\", url);\n 214 | }","lines":15,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-file-monitor.c#L200-214"}]},
{"id":"API_11","type":"api_misuse","type_label":"Api Misuse","severity":"medium","difficulty":"medium","description":"ephy_view_source get_web_view_matching_uri returns a borrowed WebKitWebView with no ref; the async webkit_web_resource_get_data is started against it with the request (which keeps request->web_view NULL in this branch) as user data. If the matching tab is closed before the data callback fires, web_resource_data_cb runs against a destroyed web_view - potential use-after-free.","fix":"Hold a ref on the matched web_view for the async operation, or cancel on its destruction.","locations":[{"ref":"embed/ephy-view-source-handler.c:216-241","code":" 216 | static WebKitWebView *\n 217 | get_web_view_matching_uri (const char *uri)\n 218 | {\n 219 | EphyEmbedShell *shell;\n 220 | GtkWindow *window;\n 221 | GList *embeds = NULL;\n 222 | GList *found;\n 223 | EphyEmbed *embed = NULL;\n 224 | \n 225 | shell = ephy_embed_shell_get_default ();\n 226 | window = gtk_application_get_active_window (GTK_APPLICATION (shell));\n 227 | \n 228 | if (!EPHY_IS_EMBED_CONTAINER (window))\n 229 | goto out;\n 230 | \n 231 | embeds = ephy_embed_container_get_children (EPHY_EMBED_CONTAINER (window));\n 232 | found = g_list_find_custom (embeds, uri, (GCompareFunc)embed_is_displaying_matching_uri);\n 233 | \n 234 | if (found)\n 235 | embed = found->data;\n 236 | \n 237 | out:\n 238 | g_list_free (embeds);\n 239 | \n 240 | return embed ? WEBKIT_WEB_VIEW (ephy_embed_get_web_view (embed)) : NULL;\n 241 | }","lines":26,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-view-source-handler.c#L216-241"},{"ref":"embed/ephy-view-source-handler.c:155-165","code":" 155 | static void\n 156 | ephy_view_source_request_begin_get_source_from_web_view (EphyViewSourceRequest *request,\n 157 | WebKitWebView *web_view)\n 158 | {\n 159 | WebKitWebResource *resource = webkit_web_view_get_main_resource (web_view);\n 160 | g_assert (resource);\n 161 | webkit_web_resource_get_data (resource,\n 162 | request->cancellable,\n 163 | (GAsyncReadyCallback)(web_resource_data_cb),\n 164 | request);\n 165 | }","lines":11,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-view-source-handler.c#L155-165"}]},
{"id":"API_12","type":"api_misuse","type_label":"Api Misuse","severity":"medium","difficulty":"medium","description":"create_web_extension_data calls json_node_get_object(node) after only g_assert(node), without verifying the node holds an object. json_from_string may yield a value/array node; json_node_get_object returns NULL and json_object_ref(NULL) criticals/crashes on malformed input from the parent process.","fix":"Check JSON_NODE_HOLDS_OBJECT(node) before json_node_get_object and handle failure gracefully.","locations":[{"ref":"embed/web-process-extension/ephy-web-process-extension.c:447-449","code":" 447 | node = json_from_string (translations_json, NULL);\n 448 | g_assert (node);\n 449 | data->translations = json_object_ref (json_node_get_object (node));","lines":3,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-web-process-extension.c#L447-449"}]},
{"id":"API_13","type":"api_misuse","type_label":"Api Misuse","severity":"medium","difficulty":"medium","description":"decrypt() passes (password_size - 3) straight to CBC_DECRYPT, which requires the length to be a multiple of AES_BLOCK_SIZE (16). Chromium v10/v11 blobs are not raw AES-128-CBC (v11 is AES-GCM with an IV and tag), so the post-prefix length is generally not block-aligned; nettle CBC_DECRYPT will then over-read / misbehave. The whole routine assumes the legacy fixed-IV 'saltysalt'/1-iteration scheme and does not correctly handle the GCM format it claims to detect.","fix":"Validate ((password_size - 3) % AES_BLOCK_SIZE) == 0 and skip otherwise; implement the proper AES-GCM path (IV + ciphertext + tag) for modern Chromium blobs.","locations":[{"ref":"lib/sync/ephy-password-import.c:82-106","code":" 82 | decrypt (unsigned char *password,\n 83 | int password_length,\n 84 | char *phrase)\n 85 | {\n 86 | struct CBC_CTX (struct aes128_ctx, AES_BLOCK_SIZE) aes;\n 87 | unsigned char iv[16] = {0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20};\n 88 | unsigned char salt[9] = {'s', 'a', 'l', 't', 'y', 's', 'a', 'l', 't'};\n 89 | unsigned char key[16];\n 90 | char *out;\n 91 | \n 92 | pbkdf2_hmac_sha1 (strlen (phrase), (unsigned char *)phrase, 1, sizeof (salt), salt, sizeof (key), key);\n 93 | \n 94 | out = g_malloc0 (password_length + 1);\n 95 | aes128_set_decrypt_key (&aes.ctx, key);\n 96 | CBC_SET_IV (&aes, iv);\n 97 | CBC_DECRYPT (&aes, aes128_decrypt, password_length, (unsigned char *)out, password);\n 98 | \n 99 | /* Remove padding */\n 100 | for (int i = 0; i < password_length; i++) {\n 101 | if (!g_ascii_isprint (out[i]))\n 102 | out[i] = '\\0';\n 103 | }\n 104 | \n 105 | return out;\n 106 | }","lines":25,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-password-import.c#L82-106"},{"ref":"lib/sync/ephy-password-import.c:191","code":" 191 | decrypted_password = decrypt ((unsigned char *)(password) + 3, password_size - 3, secret_phrase);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-password-import.c#L191"}]},
{"id":"API_14","type":"api_misuse","type_label":"Api Misuse","severity":"medium","difficulty":"medium","description":"ephy_sync_crypto_decrypt_jwe decodes the EPK x/y coordinates with ephy_sync_utils_base64_urlsafe_decode but never checks that epk_x_bytes/epk_y_bytes are non-NULL before mpz_import. It also does not validate iv_len (passed to gcm_aes256_set_iv) or that ciphertext_len > 0. A malformed JWE from the server (decode failure -> NULL, or empty/odd-length components) can cause a NULL deref or zero-length GCM operation.","fix":"Null-check epk_x_bytes/epk_y_bytes (and the decoded lengths) before mpz_import; validate iv_len and ciphertext_len before the GCM operations.","locations":[{"ref":"lib/sync/ephy-sync-crypto.c:1496-1500","code":" 1496 | epk_x_bytes = ephy_sync_utils_base64_urlsafe_decode (epk_x_b64, &epk_x_len, TRUE);\n 1497 | epk_y_bytes = ephy_sync_utils_base64_urlsafe_decode (epk_y_b64, &epk_y_len, TRUE);\n 1498 | \n 1499 | mpz_import (sx, epk_x_len, 1, 1, 1, 0, epk_x_bytes);\n 1500 | mpz_import (sy, epk_y_len, 1, 1, 1, 0, epk_y_bytes);","lines":5,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-crypto.c#L1496-1500"},{"ref":"lib/sync/ephy-sync-crypto.c:1448-1458","code":" 1448 | iv = ephy_sync_utils_base64_urlsafe_decode (parts[2], &iv_len, TRUE);\n 1449 | ciphertext = ephy_sync_utils_base64_urlsafe_decode (parts[3], &ciphertext_len, TRUE);\n 1450 | tag = ephy_sync_utils_base64_urlsafe_decode (parts[4], &tag_len, TRUE);\n 1451 | if (!iv || !ciphertext || !tag) {\n 1452 | g_warning (\"Failed to decode JWE components\");\n 1453 | return NULL;\n 1454 | }\n 1455 | if (tag_len != GCM_DIGEST_SIZE) {\n 1456 | g_warning (\"Invalid GCM tag length: %\" G_GSIZE_FORMAT, tag_len);\n 1457 | return NULL;\n 1458 | }","lines":11,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-crypto.c#L1448-1458"}]},
{"id":"API_15","type":"api_misuse","type_label":"Api Misuse","severity":"medium","difficulty":"medium","description":"ephy_sync_service_get_key_bundle does `node = json_from_string(crypto_keys, &error); g_assert(!error);` - asserting that the stored crypto_keys secret is valid JSON. crypto_keys originates from the secret store / network; if it is ever corrupted the assertion aborts the whole process instead of failing gracefully. The function already returns NULL elsewhere on error.","fix":"Replace the g_assert with an error check that logs and returns NULL (and null-check the json/collections/array/string lookups too).","locations":[{"ref":"lib/sync/ephy-sync-service.c:417-429","code":" 417 | node = json_from_string (crypto_keys, &error);\n 418 | g_assert (!error);\n 419 | json = json_node_get_object (node);\n 420 | collections = json_object_get_object_member (json, \"collections\");\n 421 | array = json_object_has_member (collections, collection) ?\n 422 | json_object_get_array_member (collections, collection) :\n 423 | json_object_get_array_member (json, \"default\");\n 424 | bundle = ephy_sync_crypto_key_bundle_new (json_array_get_string_element (array, 0),\n 425 | json_array_get_string_element (array, 1));\n 426 | \n 427 | json_node_unref (node);\n 428 | \n 429 | return bundle;","lines":13,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-service.c#L417-429"}]},
{"id":"API_16","type":"api_misuse","type_label":"Api Misuse","severity":"medium","difficulty":"medium","description":"ephy_bookmarks_import frees 'table' inside each successful sub-table block (tags at 137, bookmarks at 168) but reuses the same 'table' variable for subsequent gvdb_table_get_table calls, and the out: label also calls gvdb_table_free(table). The conditional 'tags-order' / 'bookmarks-order' blocks (142, 171) leave 'table' pointing at the order table which is never freed on the success path - the order tables obtained via gvdb_table_get_table at lines 141 and 171 are leaked (only the last assigned 'table' is freed at out:, and on the normal path that is bookmarks-order while tags-order leaks).","fix":"Free each sub-table immediately after use (gvdb_table_free at the end of the tags-order and bookmarks-order blocks) and set table = NULL, so the out: cleanup does not double-free or leak.","locations":[{"ref":"src/bookmarks/ephy-bookmarks-import.c:138-186","code":" 138 | gvdb_table_free (table);\n 139 | \n 140 | /* Get tags order table */\n 141 | /* Add tags to the bookmark manager's sequence. */\n 142 | table = gvdb_table_get_table (root_table, \"tags-order\");\n 143 | if (table) {\n 144 | list = gvdb_table_get_names (table, &length);\n 145 | for (i = 0; i < length; i++) {\n 146 | GVariant *variant = gvdb_table_get_value (table, list[i]);\n 147 | const char *variant_tag;\n 148 | \n 149 | g_variant_get (variant, \"(sa(si))\", &variant_tag, NULL);\n 150 | \n 151 | ephy_bookmarks_manager_tags_order_add_tag_variant (manager, variant);\n 152 | }\n 153 | g_strfreev (list);\n 154 | }\n 155 | \n 156 | /* Get bookmarks table */\n 157 | table = gvdb_table_get_table (root_table, \"bookmarks\");\n 158 | if (!table) {\n 159 | g_set_error (error,\n 160 | BOOKMARKS_IMPORT_ERROR,\n 161 | BOOKMARKS_IMPORT_ERROR_BOOKMARKS,\n 162 | _(\"File is not a valid Epiphany bookmarks file: missing bookmarks table\"));\n 163 | res = FALSE;\n 164 | goto out;\n 165 | }\n 166 | \n 167 | bookmarks = get_bookmarks_from_table (table);\n 168 | ephy_bookmarks_manager_add_bookmarks (manager, bookmarks);\n 169 | gvdb_table_free (table);\n 170 | \n 171 | /* Get bookmarks order table */\n 172 | table = gvdb_table_get_table (root_table, \"bookmarks-order\");\n 173 | if (table) {\n 174 | list = gvdb_table_get_names (table, &length);\n 175 | for (i = 0; i < length; i++) {\n 176 | GVariant *variant = gvdb_table_get_value (table, list[i]);\n 177 | const char *type, *item;\n 178 | int index;\n 179 | \n 180 | g_variant_get (variant, \"(ssi)\", &type, &item, &index);\n 181 | \n 182 | ephy_bookmarks_manager_add_to_bookmarks_order (manager, type, item, index);\n 183 | }\n 184 | \n 185 | g_strfreev (list);\n 186 | }","lines":49,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmarks-import.c#L138-186"},{"ref":"src/bookmarks/ephy-bookmarks-import.c:188-194","code":" 188 | out:\n 189 | if (table)\n 190 | gvdb_table_free (table);\n 191 | if (bookmarks)\n 192 | g_sequence_free (bookmarks);\n 193 | if (root_table)\n 194 | gvdb_table_free (root_table);","lines":7,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmarks-import.c#L188-194"}]},
{"id":"API_17","type":"api_misuse","type_label":"Api Misuse","severity":"medium","difficulty":"medium","description":"download_removed_cb() in ephy-action-bar.c calls gtk_widget_unparent(action_bar->downloads_button) whenever the last download is removed. If the button was never added to the toolbar (download_added_cb only appends it when a download is added, and init may have already unparented it when there were no downloads), or if it was already unparented, gtk_widget_unparent on a widget with no parent triggers a GTK critical. download_added_cb guards with gtk_widget_get_parent() before appending, but download_removed_cb has no symmetric guard.","fix":"Guard with if (gtk_widget_get_parent (action_bar->downloads_button)) before calling gtk_widget_unparent().","locations":[{"ref":"src/ephy-action-bar.c:476-483","code":" 476 | static void\n 477 | download_removed_cb (EphyDownloadsManager *manager,\n 478 | EphyDownload *download,\n 479 | EphyActionBar *action_bar)\n 480 | {\n 481 | if (!ephy_downloads_manager_get_downloads (manager))\n 482 | gtk_widget_unparent (action_bar->downloads_button);\n 483 | }","lines":8,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-action-bar.c#L476-483"},{"ref":"src/ephy-action-bar.c:451-466","code":" 451 | static void\n 452 | download_added_cb (EphyDownloadsManager *manager,\n 453 | EphyDownload *download,\n 454 | EphyActionBar *action_bar)\n 455 | {\n 456 | if (!action_bar->downloads_popover) {\n 457 | action_bar->downloads_popover = ephy_downloads_popover_new ();\n 458 | gtk_menu_button_set_popover (GTK_MENU_BUTTON (action_bar->downloads_button),\n 459 | action_bar->downloads_popover);\n 460 | }\n 461 | \n 462 | add_attention (action_bar);\n 463 | \n 464 | if (!gtk_widget_get_parent (action_bar->downloads_button))\n 465 | gtk_box_append (GTK_BOX (action_bar->toolbar), action_bar->downloads_button);\n 466 | }","lines":16,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-action-bar.c#L451-466"}]},
{"id":"API_18","type":"api_misuse","type_label":"Api Misuse","severity":"medium","difficulty":"easy","description":"timeout_id is a guint but is initialised to -1 (G_MAXUINT) in init and reset to -1 in animation_timeout_cb. The dispose handler calls g_clear_handle_id (&self->timeout_id, g_source_remove); since G_MAXUINT != 0 this invokes g_source_remove(G_MAXUINT) on a non-existent source, producing a \"Source ID was not found\" critical warning. GLib's convention is that 0 means \"no source\".","fix":"Initialise and reset timeout_id to 0 (not -1), matching the g_clear_handle_id contract.","locations":[{"ref":"src/ephy-downloads-paintable.c:178","code":" 178 | self->timeout_id = -1;","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-downloads-paintable.c#L178"},{"ref":"src/ephy-downloads-paintable.c:282","code":" 282 | self->timeout_id = -1;","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-downloads-paintable.c#L282"},{"ref":"src/ephy-downloads-paintable.c:143","code":" 143 | g_clear_handle_id (&self->timeout_id, g_source_remove);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-downloads-paintable.c#L143"}]},
{"id":"API_19","type":"api_misuse","type_label":"Api Misuse","severity":"low","difficulty":"medium","description":"reader_setting_changed_cb passes enum_nick() directly into g_strdup_printf %s; enum_nick can return NULL if g_enum_get_value fails (out-of-range setting), passing NULL to %s (undefined behavior).","fix":"Guard against a NULL nick (fall back to a default) before building the JS snippet.","locations":[{"ref":"embed/ephy-web-view.c:2446-2489","code":" 2446 | static const char *\n 2447 | enum_nick (GType enum_type,\n 2448 | int value)\n 2449 | {\n 2450 | GEnumClass *enum_class;\n 2451 | const GEnumValue *enum_value;\n 2452 | const char *nick = NULL;\n 2453 | \n 2454 | enum_class = g_type_class_ref (enum_type);\n 2455 | enum_value = g_enum_get_value (enum_class, value);\n 2456 | if (enum_value)\n 2457 | nick = enum_value->value_nick;\n 2458 | \n 2459 | g_type_class_unref (enum_class);\n 2460 | return nick;\n 2461 | }\n 2462 | \n 2463 | static void\n 2464 | reader_setting_changed_cb (EphyWebView *web_view)\n 2465 | {\n 2466 | AdwStyleManager *style_manager;\n 2467 | const gchar *font_style;\n 2468 | const gchar *color_scheme;\n 2469 | gchar *js_snippet;\n 2470 | \n 2471 | if (!g_str_has_prefix (web_view->address, EPHY_READER_SCHEME))\n 2472 | return;\n 2473 | \n 2474 | font_style = enum_nick (EPHY_TYPE_PREFS_READER_FONT_STYLE,\n 2475 | g_settings_get_enum (EPHY_SETTINGS_READER,\n 2476 | EPHY_PREFS_READER_FONT_STYLE));\n 2477 | \n 2478 | style_manager = adw_style_manager_get_default ();\n 2479 | \n 2480 | if (adw_style_manager_get_system_supports_color_schemes (style_manager))\n 2481 | color_scheme = adw_style_manager_get_dark (style_manager) ? \"dark\" : \"light\";\n 2482 | else\n 2483 | color_scheme = enum_nick (EPHY_TYPE_PREFS_READER_COLOR_SCHEME,\n 2484 | g_settings_get_enum (EPHY_SETTINGS_READER,\n 2485 | EPHY_PREFS_READER_COLOR_SCHEME));\n 2486 | \n 2487 | js_snippet = g_strdup_printf (\"document.body.className = '%s %s'\",\n 2488 | font_style,\n 2489 | color_scheme);","lines":44,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-web-view.c#L2446-2489"}]},
{"id":"API_20","type":"api_misuse","type_label":"Api Misuse","severity":"low","difficulty":"medium","description":"has_modified_forms timeout id is declared gulong but round-tripped through GINT_TO_POINTER/GPOINTER_TO_INT (gint, 32-bit signed), truncating/sign-mangling the id; g_timeout_add_seconds returns guint.","fix":"Use guint with GUINT_TO_POINTER/GPOINTER_TO_UINT.","locations":[{"ref":"embed/ephy-web-view.c:3277-3286","code":" 3277 | gulong id;\n 3278 | \n 3279 | value = webkit_web_view_evaluate_javascript_finish (view, result, &error);\n 3280 | \n 3281 | id = GPOINTER_TO_INT (g_task_get_task_data (task));\n 3282 | if (id == 0) {\n 3283 | /* We hit the timeout. Our task has already returned. */\n 3284 | goto out;\n 3285 | }\n 3286 | g_source_remove (id);","lines":10,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-web-view.c#L3277-3286"}]},
{"id":"API_21","type":"api_misuse","type_label":"Api Misuse","severity":"low","difficulty":"medium","description":"web_view_query_usernames_ready_cb and web_view_query_password_ready_cb check `if (error)` rather than the return value of webkit_web_page_send_message_to_view_finish (which may be NULL), then use the reply. on_document_loaded_message_sent_cb correctly checks the return value; this is inconsistent and can use NULL.","fix":"Check the returned WebKitUserMessage* for NULL as the failure condition.","locations":[{"ref":"embed/web-process-extension/ephy-web-process-extension.c:759-767","code":" 759 | reply = webkit_web_page_send_message_to_view_finish (web_page, result, &error);\n 760 | if (error) {\n 761 | if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))\n 762 | g_warning (\"Error getting usernames from WebView: %s\", error->message);\n 763 | g_free (data);\n 764 | return;\n 765 | }\n 766 | \n 767 | parameters = webkit_user_message_get_parameters (reply);","lines":9,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-web-process-extension.c#L759-767"},{"ref":"embed/web-process-extension/ephy-web-process-extension.c:831-839","code":" 831 | reply = webkit_web_page_send_message_to_view_finish (web_page, result, &error);\n 832 | if (error) {\n 833 | if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))\n 834 | g_warning (\"Error getting password from WebView: %s\", error->message);\n 835 | g_free (data);\n 836 | return;\n 837 | }\n 838 | \n 839 | parameters = webkit_user_message_get_parameters (reply);","lines":9,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-web-process-extension.c#L831-839"}]},
{"id":"API_22","type":"api_misuse","type_label":"Api Misuse","severity":"low","difficulty":"medium","description":"ephy_send_message passes NULL as the GCancellable to webkit_web_page_send_message_to_view, unlike other sends which pass extension->cancellable. If the page/context is torn down while the message is in flight, on_send_message_finish still runs resolve/reject on a possibly-disposed JS context.","fix":"Thread a GCancellable (cancelled on teardown) and pass it instead of NULL.","locations":[{"ref":"embed/web-process-extension/ephy-webextension-common.c:112-114","code":" 112 | webkit_web_page_send_message_to_view (send_message_data->page, message, NULL,\n 113 | (GAsyncReadyCallback)on_send_message_finish,\n 114 | ephy_callback_data_new (resolve_callback, reject_callback));","lines":3,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-webextension-common.c#L112-114"}]},
{"id":"API_23","type":"api_misuse","type_label":"Api Misuse","severity":"low","difficulty":"medium","description":"ephy_sync_crypto_decrypt_record decodes the base64 IV and ciphertext from an untrusted payload and passes them to AES-256-CBC without validating lengths: iv_len is not checked to be 16 (AES expects a 16-byte IV; a shorter buffer causes an OOB read in CBC_SET_IV) and ciphertext_len is not checked to be non-zero and a multiple of AES_BLOCK_SIZE (CBC_DECRYPT requires block-aligned length; ephy_sync_crypto_unpad also reads data[data_len-1], an OOB read when data_len == 0).","fix":"After base64-decoding, verify iv_len == IV_LEN, ciphertext_len > 0, and ciphertext_len % AES_BLOCK_SIZE == 0; bail out returning NULL otherwise.","locations":[{"ref":"lib/sync/ephy-sync-crypto.c:1153-1156","code":" 1153 | ciphertext = g_base64_decode (ciphertext_b64, &ciphertext_len);\n 1154 | iv = g_base64_decode (iv_b64, &iv_len);\n 1155 | cleartext = ephy_sync_crypto_aes_256_decrypt (ciphertext, ciphertext_len,\n 1156 | aes_key, iv);","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-crypto.c#L1153-1156"},{"ref":"lib/sync/ephy-sync-crypto.c:1053-1072","code":" 1053 | ephy_sync_crypto_unpad (const guint8 *data,\n 1054 | gsize data_len,\n 1055 | gsize block_len)\n 1056 | {\n 1057 | char *out;\n 1058 | gsize out_len;\n 1059 | gsize padding = data[data_len - 1];\n 1060 | \n 1061 | g_assert (data);\n 1062 | \n 1063 | if (padding >= 1 && padding <= block_len)\n 1064 | out_len = data_len - padding;\n 1065 | else\n 1066 | out_len = data_len;\n 1067 | \n 1068 | out = g_malloc0 (out_len + 1);\n 1069 | memcpy (out, data, out_len);\n 1070 | \n 1071 | return out;\n 1072 | }","lines":20,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-crypto.c#L1053-1072"}]},
{"id":"API_24","type":"api_misuse","type_label":"Api Misuse","severity":"low","difficulty":"easy","description":"HTML export prints a gint64 (ephy_bookmark_get_time_added) with the %ld format specifier. On 32-bit platforms (and per portability rules) long is 32-bit while gint64 is long long, causing format/argument size mismatch (truncated or garbage ADD_DATE, undefined behavior).","fix":"Use G_GINT64_FORMAT: g_string_append_printf (html, \"...ADD_DATE=\\\"%\" G_GINT64_FORMAT \"\\\"...\", ..., ephy_bookmark_get_time_added (bookmark), ...);","locations":[{"ref":"src/bookmarks/ephy-bookmarks-export.c:169-174","code":" 169 | g_string_append_printf (html,\n 170 | \"<DT><A HREF=\\\"%s\\\" ADD_DATE=\\\"%ld\\\" TAGS=\\\"%s\\\">%s<\/A>\\n\",\n 171 | ephy_bookmark_get_url (bookmark),\n 172 | ephy_bookmark_get_time_added (bookmark),\n 173 | tags ? tags->str : \"\",\n 174 | ephy_bookmark_get_title (bookmark));","lines":6,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmarks-export.c#L169-174"}]},
{"id":"API_25","type":"api_misuse","type_label":"Api Misuse","severity":"low","difficulty":"easy","description":"self->idle_id is declared as 'gint' but is passed to g_clear_handle_id (&self->idle_id, g_source_remove), which expects a guint*. This is a type mismatch (signed vs unsigned pointer) that is technically undefined / triggers compiler warnings; all other source-id fields in the struct are guint.","fix":"Change the field declaration to 'guint idle_id;'.","locations":[{"ref":"src/ephy-location-entry.c:98","code":" 98 | gint idle_id;","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-location-entry.c#L98"},{"ref":"src/ephy-location-entry.c:473","code":" 473 | g_clear_handle_id (&self->idle_id, g_source_remove);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-location-entry.c#L473"},{"ref":"src/ephy-location-entry.c:758","code":" 758 | g_clear_handle_id (&self->idle_id, g_source_remove);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-location-entry.c#L758"}]},
{"id":"API_26","type":"api_misuse","type_label":"Api Misuse","severity":"low","difficulty":"medium","description":"extension_view_handle_user_message() and content_scripts_handle_user_message() call g_variant_get(webkit_user_message_get_parameters(message), \"(&st&s)\", ...) with no check that the parameters variant is non-NULL or of the expected type. A user message from a compromised/buggy web process with NULL or mismatched parameters causes g_variant_get to assert/crash.","fix":"Fetch the parameters, verify non-NULL and g_variant_is_of_type() before g_variant_get; respond_with_error otherwise.","locations":[{"ref":"src/webextension/ephy-web-extension-manager.c:912","code":" 912 | g_variant_get (webkit_user_message_get_parameters (message), \"(&st&s)\", &guid, &frame_id, &json_string);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension-manager.c#L912"},{"ref":"src/webextension/ephy-web-extension-manager.c:976","code":" 976 | g_variant_get (webkit_user_message_get_parameters (message), \"(&st&s)\", &extension_guid, &frame_id, &json_string);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension-manager.c#L976"}]},
{"id":"PERF_1","type":"performance","type_label":"Performance","severity":"low","difficulty":"medium","description":"get_existing_bookmark does a full linear scan over ALL bookmarks for every imported URL, and it is called once per imported bookmark (and currently twice per bookmark in the firefox path). For an import of N bookmarks against M existing bookmarks this is O(N*M). For large bookmark sets the chrome/firefox/html import becomes quadratic.","fix":"Build a GHashTable url->EphyBookmark of existing bookmarks once before the import loop and look up in O(1).","locations":[{"ref":"src/bookmarks/ephy-bookmarks-import.c:199-236","code":" 199 | static EphyBookmark *\n 200 | get_existing_bookmark (const char *url,\n 201 | GSequence *tags,\n 202 | EphyBookmarksManager *manager)\n 203 | {\n 204 | GSequence *bookmarks = ephy_bookmarks_manager_get_bookmarks (manager);\n 205 | GSequenceIter *iter;\n 206 | EphyBookmark *existing_bookmark = NULL;\n 207 | \n 208 | for (iter = g_sequence_get_begin_iter (bookmarks);\n 209 | !g_sequence_iter_is_end (iter);\n 210 | iter = g_sequence_iter_next (iter)) {\n 211 | EphyBookmark *bookmark = g_sequence_get (iter);\n 212 | GSequence *bookmark_tags = ephy_bookmark_get_tags (bookmark);\n 213 | GSequenceIter *tags_iter;\n 214 | \n 215 | if (g_strcmp0 (ephy_bookmark_get_url (bookmark), url) != 0)\n 216 | continue;\n 217 | \n 218 | existing_bookmark = bookmark;\n 219 | \n 220 | /* If the bookmark already exists, add any tags the imported bookmark has\n 221 | * that the existing one doesn't. */\n 222 | for (tags_iter = g_sequence_get_begin_iter (tags);\n 223 | !g_sequence_iter_is_end (tags_iter);\n 224 | tags_iter = g_sequence_iter_next (tags_iter)) {\n 225 | const char *tag = g_sequence_get (tags_iter);\n 226 | GSequenceIter *search_iter = g_sequence_lookup (bookmark_tags, (gpointer)tag,\n 227 | (GCompareDataFunc)ephy_bookmark_tags_compare,\n 228 | NULL);\n 229 | \n 230 | if (!search_iter)\n 231 | ephy_bookmark_add_tag (bookmark, tag);\n 232 | }\n 233 | }\n 234 | \n 235 | return existing_bookmark;\n 236 | }","lines":38,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmarks-import.c#L199-236"},{"ref":"src/bookmarks/ephy-bookmarks-import.c:360-361","code":" 360 | if (get_existing_bookmark (url, tags, manager))\n 361 | g_sequence_prepend (bookmarks, get_existing_bookmark (url, tags, manager));","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmarks-import.c#L360-361"},{"ref":"src/bookmarks/ephy-bookmarks-import.c:762","code":" 762 | EphyBookmark *existing_bookmark = get_existing_bookmark (url, tags, manager);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/bookmarks/ephy-bookmarks-import.c#L762"}]},
{"id":"PERF_2","type":"performance","type_label":"Performance","severity":"low","difficulty":"easy","description":"add_search_engines() evaluates g_list_model_get_n_items (G_LIST_MODEL (manager)) on every loop iteration (in the for-condition) instead of once, turning a linear loop into repeated model size queries.","fix":"Compute n_items once before the loop and compare 'added < n_items'.","locations":[{"ref":"src/ephy-suggestion-model.c:328","code":" 328 | for (added = 0; added < g_list_model_get_n_items (G_LIST_MODEL (manager)); added++) {","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-suggestion-model.c#L328"}]},
{"id":"PERF_3","type":"performance","type_label":"Performance","severity":"low","difficulty":"easy","description":"tabs_query() iterates windows with g_list_length(windows) in the loop condition and g_list_nth_data(windows, win_idx) for each access, which is O(n^2) over the GList of windows. With many windows this is needless quadratic work.","fix":"Iterate the GList directly with a 'for (GList *l = windows; l; l = l->next)' and track win_idx with a counter.","locations":[{"ref":"src/ephy-suggestion-model.c:506-507","code":" 506 | for (guint win_idx = 0; win_idx < g_list_length (windows); win_idx++) {\n 507 | window = EPHY_WINDOW (g_list_nth_data (windows, win_idx));","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-suggestion-model.c#L506-507"}]},
{"id":"PERF_4","type":"performance","type_label":"Performance","severity":"low","difficulty":"medium","description":"filter_downloads() builds its result with repeated g_list_append (O(n) per insert -> O(n^2)). The download list is normally small, but it is an avoidable quadratic.","fix":"Use g_list_prepend and a single g_list_reverse at the end, or a GPtrArray.","locations":[{"ref":"src/webextension/api/downloads.c:620","code":" 620 | matches = g_list_append (matches, dl);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/downloads.c#L620"}]},
{"id":"PERF_5","type":"performance","type_label":"Performance","severity":"negligible","difficulty":"easy","description":"Several error-page formatters call g_strdup_printf with a translatable string and no format args, invoking printf needlessly and risking format-string issues if a translation introduces a stray '%'.","fix":"Use g_strdup(_(\"...\")) where there are no format arguments.","locations":[{"ref":"embed/ephy-web-view.c:1872","code":" 1872 | *page_title = g_strdup_printf (_(\"Problem Loading Page\"));","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-web-view.c#L1872"},{"ref":"embed/ephy-web-view.c:2107","code":" 2107 | *page_title = g_strdup_printf (_(\"File not found\"));","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-web-view.c#L2107"},{"ref":"embed/ephy-web-view.c:2117-2119","code":" 2117 | second_paragraph = g_strdup_printf (_(\"Please check the file name for \"\n 2118 | \"capitalization or other typing errors. Also check if \"\n 2119 | \"it has been moved, renamed, or deleted.\"));","lines":3,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-web-view.c#L2117-2119"}]},
{"id":"PERF_6","type":"performance","type_label":"Performance","severity":"negligible","difficulty":"easy","description":"frame_destroyed_notify removes a frame from frames_map via g_hash_table_foreach_remove scanning all entries by value on each frame teardown (O(n) per destroy) because the id cannot be recovered from the GObject pointer.","fix":"Store the frame id on the frame (g_object_set_data) or key the map by the WebKitFrame pointer for O(1) removal.","locations":[{"ref":"embed/web-process-extension/ephy-web-process-extension.c:107-126","code":" 107 | static gboolean\n 108 | remove_if_value_matches_user_data (gpointer key,\n 109 | gpointer value,\n 110 | gpointer user_data)\n 111 | {\n 112 | return value == user_data;\n 113 | }\n 114 | \n 115 | static void\n 116 | frame_destroyed_notify (EphyWebProcessExtension *extension,\n 117 | GObject *where_the_object_was)\n 118 | {\n 119 | /* This is awkward. We can't just remove the frame from the table\n 120 | * directly since we don't have any way to get its ID except by\n 121 | * checking every entry in the map....\n 122 | */\n 123 | g_hash_table_foreach_remove (extension->frames_map,\n 124 | remove_if_value_matches_user_data,\n 125 | where_the_object_was);\n 126 | }","lines":20,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-web-process-extension.c#L107-126"}]},
{"id":"PERF_7","type":"performance","type_label":"Performance","severity":"negligible","difficulty":"medium","description":"ephy_sync_utils_decode_hex() calls strlen(hex) in the loop condition on every iteration, making the loop O(n^2) in the length of the hex string.","fix":"Cache the length in a local before the loop.","locations":[{"ref":"lib/ephy-sync-utils.c:85","code":" 85 | for (gsize i = 0, j = 0; i < strlen (hex); i += 2, j++)","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/ephy-sync-utils.c#L85"}]},
{"id":"DEAD_1","type":"dead_code","type_label":"Dead Code","severity":"negligible","difficulty":"easy","description":"session_opened_cb declares g_autoptr(GList) modules and slots that are never assigned/used. default_script_world_window_object_cleared_cb declares an unused g_autoptr(JSCValue) js_i18n.","fix":"Remove the unused declarations.","locations":[{"ref":"embed/ephy-client-certificate-manager.c:295-296","code":" 295 | g_autoptr (GList) modules = NULL;\n 296 | g_autoptr (GList) slots = NULL;","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-client-certificate-manager.c#L295-296"},{"ref":"embed/web-process-extension/ephy-web-process-extension.c:990","code":" 990 | g_autoptr (JSCValue) js_i18n = NULL;","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-web-process-extension.c#L990"}]},
{"id":"DEAD_2","type":"dead_code","type_label":"Dead Code","severity":"negligible","difficulty":"easy","description":"escape_csv_field does `result = g_string_free_and_steal(escaped_field); escaped_field = NULL; return g_steal_pointer(&result);`. g_steal_pointer on a plain local that is immediately returned is pointless; the value already transfers on return.","fix":"Return directly: `return g_string_free_and_steal(g_steal_pointer(&escaped_field));`.","locations":[{"ref":"lib/sync/ephy-password-export.c:104-107","code":" 104 | result = g_string_free_and_steal (escaped_field);\n 105 | escaped_field = NULL;\n 106 | \n 107 | return g_steal_pointer (&result);","lines":4,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-password-export.c#L104-107"}]},
{"id":"CMT_1","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","difficulty":"easy","description":"Log message duplicates a word: 'removal scheduled scheduled.'","fix":"Use \"Filter %s removal scheduled.\"","locations":[{"ref":"embed/ephy-filters-manager.c:874","code":" 874 | LOG (\"Filter %s removal scheduled scheduled.\", identifier);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-filters-manager.c#L874"}]},
{"id":"CMT_2","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","difficulty":"easy","description":"Both forget_cb and replace_existing_cb carry the comment 'Note: a second copy of this comment exists in replace_existing_cb.' In replace_existing_cb that self-reference points to itself, which is wrong; it should reference forget_cb (or be removed).","fix":"Correct the cross-reference in replace_existing_cb to name forget_cb, or drop the note.","locations":[{"ref":"lib/sync/ephy-password-manager.c:907","code":" 907 | * Note: a second copy of this comment exists in replace_existing_cb().","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-password-manager.c#L907"},{"ref":"lib/sync/ephy-password-manager.c:1052","code":" 1052 | * Note: a second copy of this comment exists in replace_existing_cb().","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-password-manager.c#L1052"}]},
{"id":"CMT_3","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","difficulty":"easy","description":"Comment in write_ephy_window() says 'We are in EPHY_PREFS_RESTORE_SESSION_POLICY_NEVER with pinned tabs' but the surrounding code branch is taken when policy == EPHY_PREFS_RESTORE_SESSION_POLICY_CRASHED (only_pinned_tabs is set from POLICY_CRASHED, not POLICY_NEVER). The comment names the wrong enum value.","fix":"Update the comment to reference EPHY_PREFS_RESTORE_SESSION_POLICY_CRASHED.","locations":[{"ref":"src/ephy-session.c:782-783","code":" 782 | policy = g_settings_get_enum (EPHY_SETTINGS_MAIN, EPHY_PREFS_RESTORE_SESSION_POLICY);\n 783 | only_pinned_tabs = policy == EPHY_PREFS_RESTORE_SESSION_POLICY_CRASHED;","lines":2,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-session.c#L782-783"},{"ref":"src/ephy-session.c:852-854","code":" 852 | if (only_pinned_tabs && last_pinned_tab != -1) {\n 853 | /* We are in EPHY_PREFS_RESTORE_SESSION_POLICY_NEVER with pinned tabs\n 854 | * Create a new overview page after the pinned tab","lines":3,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-session.c#L852-854"}]},
{"id":"CMT_4","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","difficulty":"easy","description":"LOG() messages inside migrate_pre_flatpak_webapps() refer to a function name \"migrate_profile_directories\" that no longer exists (the function was renamed). The log prefixes are misleading when reading migration logs.","fix":"Update the LOG() prefixes to the current function name (migrate_pre_flatpak_webapps) or use G_STRFUNC.","locations":[{"ref":"src/profile-migrator/ephy-profile-migrator.c:137","code":" 137 | LOG (\"migrate_profile_directories: moving '%s' to '%s'\", old_desktop_path_name, new_desktop_path_name);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/profile-migrator/ephy-profile-migrator.c#L137"},{"ref":"src/profile-migrator/ephy-profile-migrator.c:195","code":" 195 | LOG (\"migrate_profile_directories: setting Icon to %s\", new_icon);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/profile-migrator/ephy-profile-migrator.c#L195"}]},
{"id":"DUP_1","type":"duplicate_code","type_label":"Duplicate Code","severity":"low","difficulty":"medium","description":"The user-stylesheet and user-javascript machinery (read_cb, output_stream_splice_cb, file_changed, update_*_on_all_ucm, webkit_pref_callback_*) are near-identical copies differing only in the global pointer and constructor; this >40-line duplication is exactly what hid the line-243 copy-paste bug.","fix":"Factor into one helper parameterized by a small struct (monitor, asset, constructor, unref, add/remove-all UCM).","locations":[{"ref":"embed/ephy-embed-prefs.c:50-262","code":" 50 | static void\n 51 | update_user_style_on_all_ucm (void)\n 52 | {\n 53 | GList *list = NULL;\n 54 | \n 55 | for (list = ucm_list; list; list = list->next) {\n 56 | WebKitUserContentManager *ucm = list->data;\n 57 | \n 58 | webkit_user_content_manager_remove_all_style_sheets (ucm);\n 59 | if (style_sheet)\n 60 | webkit_user_content_manager_add_style_sheet (ucm, style_sheet);\n 61 | }\n 62 | }\n 63 | \n 64 | static void\n 65 | user_style_sheet_output_stream_splice_cb (GOutputStream *output_stream,\n 66 | GAsyncResult *result,\n 67 | gpointer user_data)\n 68 | {\n 69 | gssize bytes;\n 70 | \n 71 | g_clear_pointer (&style_sheet, webkit_user_style_sheet_unref);\n 72 | \n 73 | bytes = g_output_stream_splice_finish (output_stream, result, NULL);\n 74 | if (bytes > 0) {\n 75 | style_sheet = webkit_user_style_sheet_new (g_memory_output_stream_get_data (G_MEMORY_OUTPUT_STREAM (output_stream)),\n 76 | WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, WEBKIT_USER_STYLE_LEVEL_USER,\n 77 | NULL, NULL);\n 78 | }\n 79 | \n 80 | update_user_style_on_all_ucm ();\n 81 | \n 82 | g_object_unref (output_stream);\n 83 | }\n 84 | \n 85 | static void\n 86 | user_style_sheet_read_cb (GFile *file,\n 87 | GAsyncResult *result,\n 88 | gpointer user_data)\n 89 | {\n 90 | g_autoptr (GFileInputStream) input_stream = NULL;\n 91 | g_autoptr (GOutputStream) output_stream = NULL;\n 92 | \n 93 | input_stream = g_file_read_finish (file, result, NULL);\n 94 | if (!input_stream)\n 95 | return;\n 96 | \n 97 | output_stream = g_memory_output_stream_new_resizable ();\n 98 | g_output_stream_splice_async (g_steal_pointer (&output_stream),\n 99 | G_INPUT_STREAM (input_stream),\n 100 | G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,\n 101 | G_PRIORITY_DEFAULT,\n 102 | NULL,\n 103 | (GAsyncReadyCallback)user_style_sheet_output_stream_splice_cb,\n 104 | NULL);\n 105 | }\n 106 | \n 107 | static void\n 108 | user_style_sheet_file_changed (GFileMonitor *monitor,\n 109 | GFile *file,\n 110 | GFile *other_file,\n 111 | GFileMonitorEvent event_type,\n 112 | gpointer user_data)\n 113 | {\n 114 | if (event_type == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT) {\n 115 | g_file_read_async (file, G_PRIORITY_DEFAULT, NULL,\n 116 | (GAsyncReadyCallback)user_style_sheet_read_cb, NULL);\n 117 | }\n 118 | }\n 119 | \n 120 | static void\n 121 | webkit_pref_callback_user_stylesheet (GSettings *settings,\n 122 | const char *key,\n 123 | gpointer data)\n 124 | {\n 125 | g_autoptr (GFile) file = NULL;\n 126 | g_autofree char *filename = NULL;\n 127 | gboolean value;\n 128 | GError *error = NULL;\n 129 | \n 130 | value = g_settings_get_boolean (settings, key);\n 131 | \n 132 | if (user_style_sheet_monitor) {\n 133 | g_signal_handlers_disconnect_by_func (user_style_sheet_monitor, user_style_sheet_file_changed, NULL);\n 134 | g_clear_object (&user_style_sheet_monitor);\n 135 | }\n 136 | \n 137 | g_clear_pointer (&style_sheet, webkit_user_style_sheet_unref);\n 138 | \n 139 | if (!value) {\n 140 | update_user_style_on_all_ucm ();\n 141 | return;\n 142 | }\n 143 | \n 144 | filename = g_build_filename (ephy_profile_dir (), USER_STYLESHEET_FILENAME, NULL);\n 145 | file = g_file_new_for_path (filename);\n 146 | \n 147 | g_file_read_async (file, G_PRIORITY_DEFAULT, NULL,\n 148 | (GAsyncReadyCallback)user_style_sheet_read_cb, NULL);\n 149 | \n 150 | user_style_sheet_monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE, NULL, &error);\n 151 | if (!user_style_sheet_monitor) {\n 152 | g_warning (\"Could not create a file monitor for %s: %s\", g_file_get_uri (file), error->message);\n 153 | g_error_free (error);\n 154 | } else {\n 155 | g_signal_connect (user_style_sheet_monitor, \"changed\", G_CALLBACK (user_style_sheet_file_changed), NULL);\n 156 | }\n 157 | }\n 158 | \n 159 | static void\n 160 | update_user_javascript_on_all_ucm (void)\n 161 | {\n 162 | GList *list = NULL;\n 163 | \n 164 | for (list = ucm_list; list; list = list->next) {\n 165 | WebKitUserContentManager *ucm = list->data;\n 166 | \n 167 | webkit_user_content_manager_remove_all_scripts (ucm);\n 168 | if (javascript)\n 169 | webkit_user_content_manager_add_script (ucm, javascript);\n 170 | }\n 171 | }\n 172 | \n 173 | static void\n 174 | user_javascript_output_stream_splice_cb (GOutputStream *output_stream,\n 175 | GAsyncResult *result,\n 176 | gpointer user_data)\n 177 | {\n 178 | gssize bytes;\n 179 | \n 180 | g_clear_pointer (&javascript, webkit_user_script_unref);\n 181 | \n 182 | bytes = g_output_stream_splice_finish (output_stream, result, NULL);\n 183 | if (bytes > 0) {\n 184 | javascript = webkit_user_script_new (g_memory_output_stream_get_data (G_MEMORY_OUTPUT_STREAM (output_stream)),\n 185 | WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END,\n 186 | NULL, NULL);\n 187 | }\n 188 | \n 189 | update_user_javascript_on_all_ucm ();\n 190 | \n 191 | g_object_unref (output_stream);\n 192 | }\n 193 | \n 194 | static void\n 195 | user_javascript_read_cb (GFile *file,\n 196 | GAsyncResult *result,\n 197 | gpointer user_data)\n 198 | {\n 199 | g_autoptr (GFileInputStream) input_stream = NULL;\n 200 | g_autoptr (GOutputStream) output_stream = NULL;\n 201 | \n 202 | input_stream = g_file_read_finish (file, result, NULL);\n 203 | if (!input_stream)\n 204 | return;\n 205 | \n 206 | output_stream = g_memory_output_stream_new_resizable ();\n 207 | g_output_stream_splice_async (g_steal_pointer (&output_stream),\n 208 | G_INPUT_STREAM (input_stream),\n 209 | G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,\n 210 | G_PRIORITY_DEFAULT,\n 211 | NULL,\n 212 | (GAsyncReadyCallback)user_javascript_output_stream_splice_cb,\n 213 | NULL);\n 214 | }\n 215 | \n 216 | static void\n 217 | user_javascript_file_changed (GFileMonitor *monitor,\n 218 | GFile *file,\n 219 | GFile *other_file,\n 220 | GFileMonitorEvent event_type,\n 221 | gpointer user_data)\n 222 | {\n 223 | if (event_type == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT) {\n 224 | g_file_read_async (file, G_PRIORITY_DEFAULT, NULL,\n 225 | (GAsyncReadyCallback)user_javascript_read_cb, NULL);\n 226 | }\n 227 | }\n 228 | \n 229 | static void\n 230 | webkit_pref_callback_user_javascript (GSettings *settings,\n 231 | const char *key,\n 232 | gpointer data)\n 233 | {\n 234 | g_autoptr (GFile) file = NULL;\n 235 | g_autofree char *filename = NULL;\n 236 | gboolean value;\n 237 | g_autoptr (GError) error = NULL;\n 238 | \n 239 | value = g_settings_get_boolean (settings, key);\n 240 | \n 241 | if (user_javascript_monitor) {\n 242 | g_signal_handlers_disconnect_by_func (user_javascript_monitor, user_javascript_file_changed, NULL);\n 243 | g_clear_object (&user_style_sheet_monitor);\n 244 | }\n 245 | \n 246 | if (!value) {\n 247 | update_user_javascript_on_all_ucm ();\n 248 | return;\n 249 | }\n 250 | \n 251 | filename = g_build_filename (ephy_profile_dir (), USER_JAVASCRIPT_FILENAME, NULL);\n 252 | file = g_file_new_for_path (filename);\n 253 | \n 254 | g_file_read_async (file, G_PRIORITY_DEFAULT, NULL,\n 255 | (GAsyncReadyCallback)user_javascript_read_cb, NULL);\n 256 | \n 257 | user_javascript_monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE, NULL, &error);\n 258 | if (!user_javascript_monitor)\n 259 | g_warning (\"Could not create a file monitor for %s: %s\", g_file_get_uri (file), error->message);\n 260 | else\n 261 | g_signal_connect (user_javascript_monitor, \"changed\", G_CALLBACK (user_javascript_file_changed), NULL);\n 262 | }","lines":213,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-embed-prefs.c#L50-262"}]},
{"id":"DUP_2","type":"duplicate_code","type_label":"Duplicate Code","severity":"low","difficulty":"medium","description":"ephy-web-view.c error-page formatters repeat an identical Reload-button setup block (label 'Reload', reloadPage postMessage action, access key, computer-fail icon) verbatim across several functions.","fix":"Factor the Reload-button setup into a helper.","locations":[{"ref":"embed/ephy-web-view.c:1899-1907","code":" 1899 | /* The button on the network error page. DO NOT ADD MNEMONICS HERE. */\n 1900 | *button_label = g_strdup (_(\"Reload\"));\n 1901 | *button_action = g_strdup_printf (\"window.webkit.messageHandlers.reloadPage.postMessage(%\" G_GUINT64_FORMAT \");\",\n 1902 | webkit_web_view_get_page_id (WEBKIT_WEB_VIEW (view)));\n 1903 | /* Mnemonic for the Reload button on browser error pages. */\n 1904 | *button_accesskey = C_(\"reload-access-key\", \"R\");\n 1905 | \n 1906 | *icon_name = \"network-error-symbolic.svg\";\n 1907 | *style = \"default\";","lines":9,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-web-view.c#L1899-1907"},{"ref":"embed/ephy-web-view.c:1952-1960","code":" 1952 | /* The button on the page crash error page. DO NOT ADD MNEMONICS HERE. */\n 1953 | *button_label = g_strdup (_(\"Reload\"));\n 1954 | *button_action = g_strdup_printf (\"window.webkit.messageHandlers.reloadPage.postMessage(%\" G_GUINT64_FORMAT \");\",\n 1955 | webkit_web_view_get_page_id (WEBKIT_WEB_VIEW (view)));\n 1956 | /* Mnemonic for the Reload button on browser error pages. */\n 1957 | *button_accesskey = C_(\"reload-access-key\", \"R\");\n 1958 | \n 1959 | *icon_name = \"computer-fail-symbolic.svg\";\n 1960 | *style = \"default\";","lines":9,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-web-view.c#L1952-1960"},{"ref":"embed/ephy-web-view.c:1988-1996","code":" 1988 | /* The button on the process crash error page. DO NOT ADD MNEMONICS HERE. */\n 1989 | *button_label = g_strdup (_(\"Reload\"));\n 1990 | *button_action = g_strdup_printf (\"window.webkit.messageHandlers.reloadPage.postMessage(%\" G_GUINT64_FORMAT \");\",\n 1991 | webkit_web_view_get_page_id (WEBKIT_WEB_VIEW (view)));\n 1992 | /* Mnemonic for the Reload button on browser error pages. */\n 1993 | *button_accesskey = C_(\"reload-access-key\", \"R\");\n 1994 | \n 1995 | *icon_name = \"computer-fail-symbolic.svg\";\n 1996 | *style = \"default\";","lines":9,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/ephy-web-view.c#L1988-1996"}]},
{"id":"DUP_3","type":"duplicate_code","type_label":"Duplicate Code","severity":"low","difficulty":"medium","description":"js_exception_handler is duplicated verbatim in ephy-web-process-extension.c and ephy-webextension-common.c (~13 identical lines each).","fix":"Move it into the shared common file and call it from both.","locations":[{"ref":"embed/web-process-extension/ephy-web-process-extension.c:935-950","code":" 935 | static void\n 936 | js_exception_handler (JSCContext *context,\n 937 | JSCException *exception)\n 938 | {\n 939 | g_autoptr (JSCValue) js_console = NULL;\n 940 | g_autoptr (JSCValue) js_result = NULL;\n 941 | g_autofree char *report = NULL;\n 942 | \n 943 | js_console = jsc_context_get_value (context, \"console\");\n 944 | js_result = jsc_value_object_invoke_method (js_console, \"error\", JSC_TYPE_EXCEPTION, exception, G_TYPE_NONE);\n 945 | (void)js_result;\n 946 | report = jsc_exception_report (exception);\n 947 | g_warning (\"%s\", report);\n 948 | \n 949 | jsc_context_throw_exception (context, exception);\n 950 | }","lines":16,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-web-process-extension.c#L935-950"},{"ref":"embed/web-process-extension/ephy-webextension-common.c:167-182","code":" 167 | static void\n 168 | js_exception_handler (JSCContext *context,\n 169 | JSCException *exception)\n 170 | {\n 171 | g_autoptr (JSCValue) js_console = NULL;\n 172 | g_autoptr (JSCValue) js_result = NULL;\n 173 | g_autofree char *report = NULL;\n 174 | \n 175 | js_console = jsc_context_get_value (context, \"console\");\n 176 | js_result = jsc_value_object_invoke_method (js_console, \"error\", JSC_TYPE_EXCEPTION, exception, G_TYPE_NONE);\n 177 | (void)js_result;\n 178 | report = jsc_exception_report (exception);\n 179 | g_warning (\"%s\", report);\n 180 | \n 181 | jsc_context_throw_exception (context, exception);\n 182 | }","lines":16,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/embed/web-process-extension/ephy-webextension-common.c#L167-182"}]},
{"id":"DUP_4","type":"duplicate_code","type_label":"Duplicate Code","severity":"low","difficulty":"medium","description":"ephy-action-bar.c and ephy-action-bar-start.c contain near-identical copies (>100 lines each) of history menu support: history_row_enter_cb, history_row_leave_cb, construct_webkit_history_list, middle_click_handle_on_history_menu_item, history_row_released_cb, icon_loaded_cb, build_history_row, build_history_menu, history_menu_closed_cb, handle_history_menu, long_pressed_cb, right_click_pressed_cb. The only differences are the owning type and a minor switch-to-new-tab flag in the -start variant. This is a large maintenance hazard (bugs must be fixed in two places).","fix":"Extract the shared history-menu construction into a common helper module taking the embed/root and cancellable, used by both action bars.","locations":[{"ref":"src/ephy-action-bar.c:82-392","code":" 82 | static void\n 83 | history_row_enter_cb (GtkEventController *controller,\n 84 | double x,\n 85 | double y,\n 86 | EphyActionBarStart *action_bar_start)\n 87 | {\n 88 | GtkWidget *widget;\n 89 | const char *text;\n 90 | GtkRoot *root;\n 91 | EphyEmbed *embed;\n 92 | WebKitWebView *web_view;\n 93 | \n 94 | widget = gtk_event_controller_get_widget (controller);\n 95 | text = g_object_get_data (G_OBJECT (widget), \"link-message\");\n 96 | \n 97 | root = gtk_widget_get_root (GTK_WIDGET (action_bar_start));\n 98 | embed = ephy_embed_container_get_active_child (EPHY_EMBED_CONTAINER (root));\n 99 | web_view = EPHY_GET_WEBKIT_WEB_VIEW_FROM_EMBED (embed);\n 100 | \n 101 | ephy_web_view_set_link_message (EPHY_WEB_VIEW (web_view), text);\n 102 | }\n 103 | \n 104 | static void\n 105 | history_row_leave_cb (GtkEventController *controller,\n 106 | EphyActionBarStart *action_bar_start)\n 107 | {\n 108 | GtkRoot *root;\n 109 | EphyEmbed *embed;\n 110 | WebKitWebView *web_view;\n 111 | \n 112 | root = gtk_widget_get_root (GTK_WIDGET (action_bar_start));\n 113 | embed = ephy_embed_container_get_active_child (EPHY_EMBED_CONTAINER (root));\n 114 | web_view = EPHY_GET_WEBKIT_WEB_VIEW_FROM_EMBED (embed);\n 115 | \n 116 | ephy_web_view_set_link_message (EPHY_WEB_VIEW (web_view), NULL);\n 117 | }\n 118 | \n 119 | static GList *\n 120 | construct_webkit_history_list (WebKitWebView *web_view,\n 121 | WebKitHistoryType hist_type,\n 122 | int limit)\n 123 | {\n 124 | WebKitBackForwardList *back_forward_list;\n 125 | \n 126 | back_forward_list = webkit_web_view_get_back_forward_list (web_view);\n 127 | return hist_type == WEBKIT_HISTORY_FORWARD ?\n 128 | g_list_reverse (webkit_back_forward_list_get_forward_list_with_limit (back_forward_list, limit)) :\n 129 | webkit_back_forward_list_get_back_list_with_limit (back_forward_list, limit);\n 130 | }\n 131 | \n 132 | static void\n 133 | middle_click_handle_on_history_menu_item (EphyEmbed *embed,\n 134 | WebKitBackForwardListItem *item)\n 135 | {\n 136 | EphyEmbed *new_embed = NULL;\n 137 | const gchar *url;\n 138 | \n 139 | new_embed = ephy_shell_new_tab (ephy_shell_get_default (),\n 140 | EPHY_WINDOW (gtk_widget_get_root (GTK_WIDGET (embed))),\n 141 | embed,\n 142 | 0);\n 143 | g_assert (new_embed);\n 144 | \n 145 | /* Load the new URL */\n 146 | url = webkit_back_forward_list_item_get_original_uri (item);\n 147 | ephy_web_view_load_url (ephy_embed_get_web_view (new_embed), url);\n 148 | }\n 149 | \n 150 | static void\n 151 | history_row_released_cb (GtkGesture *gesture,\n 152 | int n_click,\n 153 | double x,\n 154 | double y,\n 155 | EphyActionBar *action_bar)\n 156 | {\n 157 | WebKitBackForwardListItem *item;\n 158 | GtkWidget *widget;\n 159 | guint button;\n 160 | GtkRoot *root;\n 161 | EphyEmbed *embed;\n 162 | \n 163 | widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture));\n 164 | \n 165 | if (!gtk_widget_contains (widget, x, y)) {\n 166 | gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);\n 167 | return;\n 168 | }\n 169 | \n 170 | button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));\n 171 | \n 172 | if (button != GDK_BUTTON_PRIMARY && button != GDK_BUTTON_MIDDLE) {\n 173 | gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);\n 174 | return;\n 175 | }\n 176 | \n 177 | gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);\n 178 | \n 179 | root = gtk_widget_get_root (GTK_WIDGET (action_bar));\n 180 | embed = ephy_embed_container_get_active_child (EPHY_EMBED_CONTAINER (root));\n 181 | \n 182 | item = WEBKIT_BACK_FORWARD_LIST_ITEM (g_object_get_data (G_OBJECT (widget), HISTORY_ITEM_DATA_KEY));\n 183 | \n 184 | if (button == GDK_BUTTON_MIDDLE) {\n 185 | middle_click_handle_on_history_menu_item (embed, item);\n 186 | } else {\n 187 | WebKitWebView *web_view;\n 188 | \n 189 | web_view = EPHY_GET_WEBKIT_WEB_VIEW_FROM_EMBED (embed);\n 190 | webkit_web_view_go_to_back_forward_list_item (web_view, item);\n 191 | \n 192 | gtk_popover_popdown (GTK_POPOVER (action_bar->history_menu));\n 193 | }\n 194 | }\n 195 | \n 196 | static void\n 197 | icon_loaded_cb (GObject *source,\n 198 | GAsyncResult *result,\n 199 | gpointer user_data)\n 200 | {\n 201 | WebKitFaviconDatabase *database = WEBKIT_FAVICON_DATABASE (source);\n 202 | g_autoptr (GtkWidget) image = user_data;\n 203 | g_autoptr (GIcon) favicon = NULL;\n 204 | g_autoptr (GError) error = NULL;\n 205 | g_autoptr (GdkTexture) icon_texture = webkit_favicon_database_get_favicon_finish (database, result, &error);\n 206 | int scale;\n 207 | \n 208 | if (error) {\n 209 | if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))\n 210 | return;\n 211 | \n 212 | g_debug (\"Could not get icon: %s\", error->message);\n 213 | return;\n 214 | }\n 215 | \n 216 | if (!icon_texture)\n 217 | return;\n 218 | \n 219 | scale = gtk_widget_get_scale_factor (image);\n 220 | favicon = ephy_favicon_get_from_texture_scaled (icon_texture, FAVICON_SIZE * scale, FAVICON_SIZE * scale);\n 221 | if (favicon)\n 222 | gtk_image_set_from_gicon (GTK_IMAGE (image), favicon);\n 223 | }\n 224 | \n 225 | static GtkWidget *\n 226 | build_history_row (EphyActionBar *action_bar,\n 227 | WebKitBackForwardListItem *item)\n 228 | {\n 229 | EphyEmbedShell *shell = ephy_embed_shell_get_default ();\n 230 | const char *uri;\n 231 | g_autofree char *title = NULL;\n 232 | WebKitFaviconDatabase *database;\n 233 | GtkWidget *row, *box, *icon, *label;\n 234 | GtkEventController *controller;\n 235 | GtkGesture *gesture;\n 236 | \n 237 | uri = webkit_back_forward_list_item_get_uri (item);\n 238 | title = g_strdup (webkit_back_forward_list_item_get_title (item));\n 239 | \n 240 | row = gtk_list_box_row_new ();\n 241 | \n 242 | box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);\n 243 | gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), box);\n 244 | \n 245 | icon = gtk_image_new ();\n 246 | gtk_image_set_pixel_size (GTK_IMAGE (icon), 16);\n 247 | gtk_box_append (GTK_BOX (box), icon);\n 248 | \n 249 | label = gtk_label_new (NULL);\n 250 | gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);\n 251 | gtk_label_set_max_width_chars (GTK_LABEL (label), MAX_LABEL_LENGTH);\n 252 | gtk_label_set_single_line_mode (GTK_LABEL (label), TRUE);\n 253 | gtk_label_set_xalign (GTK_LABEL (label), 0.0f);\n 254 | gtk_widget_set_hexpand (label, TRUE);\n 255 | gtk_box_append (GTK_BOX (box), label);\n 256 | \n 257 | g_object_set_data_full (G_OBJECT (row), HISTORY_ITEM_DATA_KEY,\n 258 | g_object_ref (item), g_object_unref);\n 259 | \n 260 | if (title && *title)\n 261 | gtk_label_set_label (GTK_LABEL (label), title);\n 262 | else\n 263 | gtk_label_set_label (GTK_LABEL (label), uri);\n 264 | \n 265 | database = ephy_embed_shell_get_favicon_database (shell);\n 266 | webkit_favicon_database_get_favicon (database, uri,\n 267 | action_bar->cancellable,\n 268 | (GAsyncReadyCallback)icon_loaded_cb,\n 269 | g_object_ref (icon));\n 270 | \n 271 | g_object_set_data_full (G_OBJECT (row), \"link-message\",\n 272 | g_strdup (uri), (GDestroyNotify)g_free);\n 273 | \n 274 | controller = gtk_event_controller_motion_new ();\n 275 | g_signal_connect (controller, \"enter\", G_CALLBACK (history_row_enter_cb), action_bar);\n 276 | g_signal_connect (controller, \"leave\", G_CALLBACK (history_row_leave_cb), action_bar);\n 277 | gtk_widget_add_controller (row, controller);\n 278 | \n 279 | gesture = gtk_gesture_click_new ();\n 280 | gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), 0);\n 281 | g_signal_connect (gesture, \"released\", G_CALLBACK (history_row_released_cb), action_bar);\n 282 | gtk_widget_add_controller (row, GTK_EVENT_CONTROLLER (gesture));\n 283 | \n 284 | return row;\n 285 | }\n 286 | \n 287 | static void\n 288 | build_history_menu (EphyActionBar *action_bar,\n 289 | GtkWidget *parent,\n 290 | EphyNavigationHistoryDirection direction)\n 291 | {\n 292 | GtkWidget *popover, *listbox;\n 293 | GtkRoot *root;\n 294 | EphyEmbed *embed;\n 295 | WebKitWebView *web_view;\n 296 | g_autoptr (GList) list = NULL;\n 297 | GList *l;\n 298 | \n 299 | root = gtk_widget_get_root (GTK_WIDGET (action_bar));\n 300 | embed = ephy_embed_container_get_active_child (EPHY_EMBED_CONTAINER (root));\n 301 | web_view = EPHY_GET_WEBKIT_WEB_VIEW_FROM_EMBED (embed);\n 302 | \n 303 | if (direction == EPHY_NAVIGATION_HISTORY_DIRECTION_BACK)\n 304 | list = construct_webkit_history_list (web_view,\n 305 | WEBKIT_HISTORY_BACKWARD, 10);\n 306 | else\n 307 | list = construct_webkit_history_list (web_view,\n 308 | WEBKIT_HISTORY_FORWARD, 10);\n 309 | \n 310 | popover = gtk_popover_new ();\n 311 | gtk_popover_set_has_arrow (GTK_POPOVER (popover), FALSE);\n 312 | gtk_widget_set_halign (popover, GTK_ALIGN_START);\n 313 | gtk_widget_add_css_class (popover, \"menu\");\n 314 | gtk_widget_set_parent (popover, parent);\n 315 | \n 316 | listbox = gtk_list_box_new ();\n 317 | gtk_popover_set_child (GTK_POPOVER (popover), listbox);\n 318 | \n 319 | for (l = list; l; l = l->next) {\n 320 | GtkWidget *row = build_history_row (action_bar, l->data);\n 321 | \n 322 | gtk_list_box_append (GTK_LIST_BOX (listbox), row);\n 323 | }\n 324 | \n 325 | action_bar->history_menu = popover;\n 326 | }\n 327 | \n 328 | static void\n 329 | history_menu_closed_cb (EphyActionBar *action_bar)\n 330 | {\n 331 | GtkWidget *parent = gtk_widget_get_parent (action_bar->history_menu);\n 332 | \n 333 | g_clear_pointer (&action_bar->history_menu, gtk_widget_unparent);\n 334 | gtk_widget_unset_state_flags (parent, GTK_STATE_FLAG_CHECKED);\n 335 | \n 336 | g_cancellable_cancel (action_bar->cancellable);\n 337 | g_clear_object (&action_bar->cancellable);\n 338 | action_bar->cancellable = g_cancellable_new ();\n 339 | }\n 340 | \n 341 | static void\n 342 | handle_history_menu (EphyActionBar *action_bar,\n 343 | double x,\n 344 | double y,\n 345 | GtkGesture *gesture)\n 346 | {\n 347 | GtkWidget *widget;\n 348 | EphyNavigationHistoryDirection direction;\n 349 | \n 350 | widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture));\n 351 | \n 352 | if (!gtk_widget_contains (widget, x, y)) {\n 353 | gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);\n 354 | return;\n 355 | }\n 356 | \n 357 | if (widget == action_bar->navigation_back)\n 358 | direction = EPHY_NAVIGATION_HISTORY_DIRECTION_BACK;\n 359 | else if (widget == action_bar->navigation_forward)\n 360 | direction = EPHY_NAVIGATION_HISTORY_DIRECTION_FORWARD;\n 361 | else\n 362 | g_assert_not_reached ();\n 363 | \n 364 | build_history_menu (action_bar, widget, direction);\n 365 | \n 366 | gtk_popover_popup (GTK_POPOVER (action_bar->history_menu));\n 367 | gtk_widget_set_state_flags (widget, GTK_STATE_FLAG_CHECKED, FALSE);\n 368 | \n 369 | g_signal_connect_swapped (action_bar->history_menu, \"closed\",\n 370 | G_CALLBACK (history_menu_closed_cb), action_bar);\n 371 | \n 372 | gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);\n 373 | }\n 374 | \n 375 | static void\n 376 | long_pressed_cb (GtkGesture *gesture,\n 377 | double x,\n 378 | double y,\n 379 | EphyActionBar *action_bar)\n 380 | {\n 381 | handle_history_menu (action_bar, x, y, gesture);\n 382 | }\n 383 | \n 384 | static void\n 385 | right_click_pressed_cb (GtkGesture *gesture,\n 386 | int n_click,\n 387 | double x,\n 388 | double y,\n 389 | EphyActionBar *action_bar)\n 390 | {\n 391 | handle_history_menu (action_bar, x, y, gesture);\n 392 | }","lines":311,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-action-bar.c#L82-392"},{"ref":"src/ephy-action-bar-start.c:71-379","code":" 71 | static void\n 72 | history_row_enter_cb (GtkEventController *controller,\n 73 | double x,\n 74 | double y,\n 75 | EphyActionBarStart *action_bar_start)\n 76 | {\n 77 | GtkWidget *widget;\n 78 | const char *text;\n 79 | GtkRoot *root;\n 80 | EphyEmbed *embed;\n 81 | WebKitWebView *web_view;\n 82 | \n 83 | widget = gtk_event_controller_get_widget (controller);\n 84 | text = g_object_get_data (G_OBJECT (widget), \"link-message\");\n 85 | \n 86 | root = gtk_widget_get_root (GTK_WIDGET (action_bar_start));\n 87 | embed = ephy_embed_container_get_active_child (EPHY_EMBED_CONTAINER (root));\n 88 | web_view = EPHY_GET_WEBKIT_WEB_VIEW_FROM_EMBED (embed);\n 89 | \n 90 | ephy_web_view_set_link_message (EPHY_WEB_VIEW (web_view), text);\n 91 | }\n 92 | \n 93 | static void\n 94 | history_row_leave_cb (GtkEventController *controller,\n 95 | EphyActionBarStart *action_bar_start)\n 96 | {\n 97 | GtkRoot *root;\n 98 | EphyEmbed *embed;\n 99 | WebKitWebView *web_view;\n 100 | \n 101 | root = gtk_widget_get_root (GTK_WIDGET (action_bar_start));\n 102 | embed = ephy_embed_container_get_active_child (EPHY_EMBED_CONTAINER (root));\n 103 | web_view = EPHY_GET_WEBKIT_WEB_VIEW_FROM_EMBED (embed);\n 104 | \n 105 | ephy_web_view_set_link_message (EPHY_WEB_VIEW (web_view), NULL);\n 106 | }\n 107 | \n 108 | static void\n 109 | middle_click_handle_on_history_menu_item (EphyEmbed *embed,\n 110 | WebKitBackForwardListItem *item)\n 111 | {\n 112 | EphyEmbed *new_embed = NULL;\n 113 | EphyNewTabFlags flags = 0;\n 114 | const gchar *url;\n 115 | \n 116 | if (g_settings_get_boolean (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_SWITCH_TO_NEW_TAB))\n 117 | flags |= EPHY_NEW_TAB_JUMP;\n 118 | \n 119 | new_embed = ephy_shell_new_tab (ephy_shell_get_default (),\n 120 | EPHY_WINDOW (gtk_widget_get_root (GTK_WIDGET (embed))),\n 121 | embed,\n 122 | flags);\n 123 | g_assert (new_embed);\n 124 | \n 125 | /* Load the new URL */\n 126 | url = webkit_back_forward_list_item_get_original_uri (item);\n 127 | ephy_web_view_load_url (ephy_embed_get_web_view (new_embed), url);\n 128 | }\n 129 | \n 130 | static void\n 131 | history_row_released_cb (GtkGesture *gesture,\n 132 | int n_click,\n 133 | double x,\n 134 | double y,\n 135 | EphyActionBarStart *action_bar_start)\n 136 | {\n 137 | WebKitBackForwardListItem *item;\n 138 | GtkWidget *widget;\n 139 | guint button;\n 140 | GtkRoot *root;\n 141 | EphyEmbed *embed;\n 142 | \n 143 | widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture));\n 144 | \n 145 | if (!gtk_widget_contains (widget, x, y)) {\n 146 | gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);\n 147 | return;\n 148 | }\n 149 | \n 150 | button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));\n 151 | \n 152 | if (button != GDK_BUTTON_PRIMARY && button != GDK_BUTTON_MIDDLE) {\n 153 | gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);\n 154 | return;\n 155 | }\n 156 | \n 157 | gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);\n 158 | \n 159 | root = gtk_widget_get_root (GTK_WIDGET (action_bar_start));\n 160 | embed = ephy_embed_container_get_active_child (EPHY_EMBED_CONTAINER (root));\n 161 | \n 162 | item = WEBKIT_BACK_FORWARD_LIST_ITEM (g_object_get_data (G_OBJECT (widget), HISTORY_ITEM_DATA_KEY));\n 163 | \n 164 | if (button == GDK_BUTTON_MIDDLE) {\n 165 | middle_click_handle_on_history_menu_item (embed, item);\n 166 | \n 167 | if (g_settings_get_boolean (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_SWITCH_TO_NEW_TAB))\n 168 | gtk_popover_popdown (GTK_POPOVER (action_bar_start->history_menu));\n 169 | } else {\n 170 | WebKitWebView *web_view;\n 171 | \n 172 | web_view = EPHY_GET_WEBKIT_WEB_VIEW_FROM_EMBED (embed);\n 173 | webkit_web_view_go_to_back_forward_list_item (web_view, item);\n 174 | \n 175 | gtk_popover_popdown (GTK_POPOVER (action_bar_start->history_menu));\n 176 | }\n 177 | }\n 178 | \n 179 | static void\n 180 | icon_loaded_cb (GObject *source,\n 181 | GAsyncResult *result,\n 182 | gpointer user_data)\n 183 | {\n 184 | WebKitFaviconDatabase *database = WEBKIT_FAVICON_DATABASE (source);\n 185 | g_autoptr (GtkWidget) image = user_data;\n 186 | g_autoptr (GIcon) favicon = NULL;\n 187 | g_autoptr (GdkTexture) icon_texture = webkit_favicon_database_get_favicon_finish (database, result, NULL);\n 188 | int scale;\n 189 | \n 190 | if (!icon_texture)\n 191 | return;\n 192 | \n 193 | scale = gtk_widget_get_scale_factor (image);\n 194 | favicon = ephy_favicon_get_from_texture_scaled (icon_texture, FAVICON_SIZE * scale, FAVICON_SIZE * scale);\n 195 | if (favicon)\n 196 | gtk_image_set_from_gicon (GTK_IMAGE (image), favicon);\n 197 | }\n 198 | \n 199 | static GList *\n 200 | construct_webkit_history_list (WebKitWebView *web_view,\n 201 | WebKitHistoryType hist_type,\n 202 | int limit)\n 203 | {\n 204 | WebKitBackForwardList *back_forward_list;\n 205 | \n 206 | back_forward_list = webkit_web_view_get_back_forward_list (web_view);\n 207 | return hist_type == WEBKIT_HISTORY_FORWARD ?\n 208 | g_list_reverse (webkit_back_forward_list_get_forward_list_with_limit (back_forward_list, limit)) :\n 209 | webkit_back_forward_list_get_back_list_with_limit (back_forward_list, limit);\n 210 | }\n 211 | \n 212 | static GtkWidget *\n 213 | build_history_row (EphyActionBarStart *action_bar_start,\n 214 | WebKitBackForwardListItem *item)\n 215 | {\n 216 | EphyEmbedShell *shell = ephy_embed_shell_get_default ();\n 217 | const char *uri;\n 218 | g_autofree char *title = NULL;\n 219 | WebKitFaviconDatabase *database;\n 220 | GtkWidget *row, *box, *icon, *label;\n 221 | GtkEventController *controller;\n 222 | GtkGesture *gesture;\n 223 | \n 224 | uri = webkit_back_forward_list_item_get_uri (item);\n 225 | title = g_strdup (webkit_back_forward_list_item_get_title (item));\n 226 | \n 227 | row = gtk_list_box_row_new ();\n 228 | \n 229 | box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);\n 230 | gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), box);\n 231 | \n 232 | icon = gtk_image_new ();\n 233 | gtk_image_set_pixel_size (GTK_IMAGE (icon), 16);\n 234 | gtk_box_append (GTK_BOX (box), icon);\n 235 | \n 236 | label = gtk_label_new (NULL);\n 237 | gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);\n 238 | gtk_label_set_max_width_chars (GTK_LABEL (label), MAX_LABEL_LENGTH);\n 239 | gtk_label_set_single_line_mode (GTK_LABEL (label), TRUE);\n 240 | gtk_label_set_xalign (GTK_LABEL (label), 0.0f);\n 241 | gtk_widget_set_hexpand (label, TRUE);\n 242 | gtk_box_append (GTK_BOX (box), label);\n 243 | \n 244 | g_object_set_data_full (G_OBJECT (row), HISTORY_ITEM_DATA_KEY,\n 245 | g_object_ref (item), g_object_unref);\n 246 | \n 247 | if (title && *title)\n 248 | gtk_label_set_label (GTK_LABEL (label), title);\n 249 | else\n 250 | gtk_label_set_label (GTK_LABEL (label), uri);\n 251 | \n 252 | database = ephy_embed_shell_get_favicon_database (shell);\n 253 | webkit_favicon_database_get_favicon (database, uri,\n 254 | action_bar_start->cancellable,\n 255 | (GAsyncReadyCallback)icon_loaded_cb,\n 256 | g_object_ref (icon));\n 257 | \n 258 | g_object_set_data_full (G_OBJECT (row), \"link-message\",\n 259 | g_strdup (uri), (GDestroyNotify)g_free);\n 260 | \n 261 | controller = gtk_event_controller_motion_new ();\n 262 | g_signal_connect (controller, \"enter\", G_CALLBACK (history_row_enter_cb), action_bar_start);\n 263 | g_signal_connect (controller, \"leave\", G_CALLBACK (history_row_leave_cb), action_bar_start);\n 264 | gtk_widget_add_controller (row, controller);\n 265 | \n 266 | gesture = gtk_gesture_click_new ();\n 267 | gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), 0);\n 268 | g_signal_connect (gesture, \"released\", G_CALLBACK (history_row_released_cb), action_bar_start);\n 269 | gtk_widget_add_controller (row, GTK_EVENT_CONTROLLER (gesture));\n 270 | \n 271 | return row;\n 272 | }\n 273 | \n 274 | static void\n 275 | build_history_menu (EphyActionBarStart *action_bar_start,\n 276 | GtkWidget *parent,\n 277 | EphyNavigationHistoryDirection direction)\n 278 | {\n 279 | GtkWidget *popover, *listbox;\n 280 | GtkRoot *root;\n 281 | EphyEmbed *embed;\n 282 | WebKitWebView *web_view;\n 283 | g_autoptr (GList) list = NULL;\n 284 | GList *l;\n 285 | \n 286 | root = gtk_widget_get_root (GTK_WIDGET (action_bar_start));\n 287 | embed = ephy_embed_container_get_active_child (EPHY_EMBED_CONTAINER (root));\n 288 | web_view = EPHY_GET_WEBKIT_WEB_VIEW_FROM_EMBED (embed);\n 289 | \n 290 | if (direction == EPHY_NAVIGATION_HISTORY_DIRECTION_BACK)\n 291 | list = construct_webkit_history_list (web_view,\n 292 | WEBKIT_HISTORY_BACKWARD, 10);\n 293 | else\n 294 | list = construct_webkit_history_list (web_view,\n 295 | WEBKIT_HISTORY_FORWARD, 10);\n 296 | \n 297 | popover = gtk_popover_new ();\n 298 | gtk_popover_set_has_arrow (GTK_POPOVER (popover), FALSE);\n 299 | gtk_widget_set_halign (popover, GTK_ALIGN_START);\n 300 | gtk_widget_add_css_class (popover, \"menu\");\n 301 | gtk_widget_set_parent (popover, parent);\n 302 | \n 303 | listbox = gtk_list_box_new ();\n 304 | gtk_popover_set_child (GTK_POPOVER (popover), listbox);\n 305 | \n 306 | for (l = list; l; l = l->next) {\n 307 | GtkWidget *row = build_history_row (action_bar_start, l->data);\n 308 | \n 309 | gtk_list_box_append (GTK_LIST_BOX (listbox), row);\n 310 | }\n 311 | \n 312 | action_bar_start->history_menu = popover;\n 313 | }\n 314 | \n 315 | static void\n 316 | history_menu_closed_cb (EphyActionBarStart *action_bar_start)\n 317 | {\n 318 | GtkWidget *parent = gtk_widget_get_parent (action_bar_start->history_menu);\n 319 | \n 320 | g_clear_pointer (&action_bar_start->history_menu, gtk_widget_unparent);\n 321 | gtk_widget_unset_state_flags (parent, GTK_STATE_FLAG_CHECKED);\n 322 | \n 323 | g_cancellable_cancel (action_bar_start->cancellable);\n 324 | g_clear_object (&action_bar_start->cancellable);\n 325 | action_bar_start->cancellable = g_cancellable_new ();\n 326 | }\n 327 | \n 328 | static void\n 329 | handle_history_menu (EphyActionBarStart *action_bar_start,\n 330 | double x,\n 331 | double y,\n 332 | GtkGesture *gesture)\n 333 | {\n 334 | GtkWidget *widget;\n 335 | EphyNavigationHistoryDirection direction;\n 336 | \n 337 | widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture));\n 338 | \n 339 | if (!gtk_widget_contains (widget, x, y)) {\n 340 | gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);\n 341 | return;\n 342 | }\n 343 | \n 344 | if (widget == action_bar_start->navigation_back)\n 345 | direction = EPHY_NAVIGATION_HISTORY_DIRECTION_BACK;\n 346 | else if (widget == action_bar_start->navigation_forward)\n 347 | direction = EPHY_NAVIGATION_HISTORY_DIRECTION_FORWARD;\n 348 | else\n 349 | g_assert_not_reached ();\n 350 | \n 351 | build_history_menu (action_bar_start, widget, direction);\n 352 | \n 353 | gtk_popover_popup (GTK_POPOVER (action_bar_start->history_menu));\n 354 | gtk_widget_set_state_flags (widget, GTK_STATE_FLAG_CHECKED, FALSE);\n 355 | \n 356 | g_signal_connect_swapped (action_bar_start->history_menu, \"closed\",\n 357 | G_CALLBACK (history_menu_closed_cb), action_bar_start);\n 358 | \n 359 | gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);\n 360 | }\n 361 | \n 362 | static void\n 363 | long_pressed_cb (GtkGesture *gesture,\n 364 | double x,\n 365 | double y,\n 366 | EphyActionBarStart *action_bar_start)\n 367 | {\n 368 | handle_history_menu (action_bar_start, x, y, gesture);\n 369 | }\n 370 | \n 371 | static void\n 372 | right_click_pressed_cb (GtkGesture *gesture,\n 373 | int n_click,\n 374 | double x,\n 375 | double y,\n 376 | EphyActionBarStart *action_bar_start)\n 377 | {\n 378 | handle_history_menu (action_bar_start, x, y, gesture);\n 379 | }","lines":309,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-action-bar-start.c#L71-379"}]},
{"id":"DUP_5","type":"duplicate_code","type_label":"Duplicate Code","severity":"low","difficulty":"medium","description":"browseraction_handler_set_badge_text and set_badge_background_color share ~25 lines of near-identical structure (get details object, NULL-check, read string field, NULL-check, read tabId/windowId, and two identical tabId/windowId rejection blocks).","fix":"Extract a helper that validates details and the tabId/windowId combination and returns the looked-up field (or sets the task error).","locations":[{"ref":"src/webextension/api/browseraction.c:44-67","code":" 44 | details = ephy_json_array_get_object (args, 0);\n 45 | if (!details) {\n 46 | g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, \"tabs.setBadgeText(): Missing details\");\n 47 | return;\n 48 | }\n 49 | \n 50 | badge_text = ephy_json_object_get_string (details, \"text\");\n 51 | if (!badge_text) {\n 52 | g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, \"browserAction.setBadgeText(): Missing text\");\n 53 | return;\n 54 | }\n 55 | \n 56 | tab_id = ephy_json_object_get_int (details, \"tabId\");\n 57 | window_id = ephy_json_object_get_int (details, \"windowId\");\n 58 | if (tab_id != -1 && window_id != -1) {\n 59 | g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, \"browserAction.setBadgeText(): tabId and windowId defined. Not supported\");\n 60 | return;\n 61 | }\n 62 | \n 63 | /* Currently only Firefox can handle them, skip it for the moment */\n 64 | if (tab_id != -1 || window_id != -1) {\n 65 | g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, \"browserAction.setBadgeText(): tabId or windowId defined. Not supported\");\n 66 | return;\n 67 | }","lines":24,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/browseraction.c#L44-67"},{"ref":"src/webextension/api/browseraction.c:86-109","code":" 86 | details = ephy_json_array_get_object (args, 0);\n 87 | if (!details) {\n 88 | g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, \"tabs.setBadgeBackgroundColor(): Missing details\");\n 89 | return;\n 90 | }\n 91 | \n 92 | badge_color = ephy_json_object_get_string (details, \"color\");\n 93 | if (!badge_color) {\n 94 | g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, \"browserAction.setBadgeBackgroundColor(): Missing badge color\");\n 95 | return;\n 96 | }\n 97 | \n 98 | tab_id = ephy_json_object_get_int (details, \"tabId\");\n 99 | window_id = ephy_json_object_get_int (details, \"windowId\");\n 100 | if (tab_id != -1 && window_id != -1) {\n 101 | g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, \"browserAction.setBadgeBackgroundColor(): tabId and windowId defined. Not supported\");\n 102 | return;\n 103 | }\n 104 | \n 105 | /* Currently only Firefox can handle them, skip it for the moment */\n 106 | if (tab_id != -1 || window_id != -1) {\n 107 | g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, \"browserAction.setBadgeBackgroundColor(): tabId or windowId defined. Not supported\");\n 108 | return;\n 109 | }","lines":24,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/browseraction.c#L86-109"}]},
{"id":"DUP_6","type":"duplicate_code","type_label":"Duplicate Code","severity":"negligible","difficulty":"medium","description":"The block that derives a SyncCryptoKeyBundle from the master key secret (if hex_len == 128 split into aes/hmac halves else decode hex and call derive_master_bundle) is copy-pasted verbatim in ephy_sync_service_upload_crypto_keys and get_crypto_keys_cb.","fix":"Factor out a helper, e.g. ephy_sync_service_make_master_bundle(const char *kb_hex).","locations":[{"ref":"lib/sync/ephy-sync-service.c:1980-1993","code":" 1980 | {\n 1981 | gsize hex_len = strlen (kb_hex);\n 1982 | \n 1983 | if (hex_len == 128) {\n 1984 | char *aes_hex = g_strndup (kb_hex, 64);\n 1985 | char *hmac_hex = g_strndup (kb_hex + 64, 64);\n 1986 | bundle = g_new (SyncCryptoKeyBundle, 1);\n 1987 | bundle->aes_key_hex = aes_hex;\n 1988 | bundle->hmac_key_hex = hmac_hex;\n 1989 | } else {\n 1990 | kb = ephy_sync_utils_decode_hex (kb_hex);\n 1991 | bundle = ephy_sync_crypto_derive_master_bundle (kb);\n 1992 | }\n 1993 | }","lines":14,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-service.c#L1980-1993"},{"ref":"lib/sync/ephy-sync-service.c:2063-2078","code":" 2063 | {\n 2064 | const char *master_key_hex = ephy_sync_service_get_secret (self, secrets[MASTER_KEY]);\n 2065 | gsize hex_len = master_key_hex ? strlen (master_key_hex) : 0;\n 2066 | \n 2067 | if (hex_len == 128) {\n 2068 | /* OAuth scoped key: first 32 bytes = AES, last 32 bytes = HMAC. */\n 2069 | char *aes_hex = g_strndup (master_key_hex, 64);\n 2070 | char *hmac_hex = g_strndup (master_key_hex + 64, 64);\n 2071 | bundle = g_new (SyncCryptoKeyBundle, 1);\n 2072 | bundle->aes_key_hex = aes_hex;\n 2073 | bundle->hmac_key_hex = hmac_hex;\n 2074 | } else {\n 2075 | kb = ephy_sync_utils_decode_hex (master_key_hex);\n 2076 | bundle = ephy_sync_crypto_derive_master_bundle (kb);\n 2077 | }\n 2078 | }","lines":16,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-service.c#L2063-2078"}]},
{"id":"DUP_7","type":"duplicate_code","type_label":"Duplicate Code","severity":"negligible","difficulty":"medium","description":"reset_all_toast_button_clicked_cb and reset_all_row_activated_cb duplicate the full nested group/row traversal scaffolding including the fragile triple gtk_widget_get_*_child walk used to locate the switch/button/feature for each row (~25 lines of near-identical structure). The two default_search_engine and incognito_search_engine get/set mapping function pairs in prefs-general-page.c are also near-identical duplicates.","fix":"Extract a helper that iterates feature rows and yields (switch_widget,button,feature); factor the search-engine mapping pair into one parameterized helper.","locations":[{"ref":"src/preferences/prefs-features-page.c:204-280","code":" 204 | static void\n 205 | reset_all_toast_button_clicked_cb (PrefsFeaturesPage *self)\n 206 | {\n 207 | WebKitSettings *settings = ephy_embed_prefs_get_settings ();\n 208 | AdwPreferencesGroup *group;\n 209 | int i = 1;\n 210 | \n 211 | while ((group = adw_preferences_page_get_group (ADW_PREFERENCES_PAGE (self), i++))) {\n 212 | GtkWidget *row;\n 213 | int j = 0;\n 214 | \n 215 | while ((row = adw_preferences_group_get_row (group, j++))) {\n 216 | GtkWidget *switch_widget = adw_action_row_get_activatable_widget (ADW_ACTION_ROW (row));\n 217 | GtkWidget *button = gtk_widget_get_last_child (gtk_widget_get_last_child (gtk_widget_get_first_child (row)));\n 218 | WebKitFeature *feature = g_object_get_data (G_OBJECT (row), \"feature\");\n 219 | const char *prev_value = g_object_steal_data (G_OBJECT (row), \"prev-value\");\n 220 | gboolean enabled;\n 221 | \n 222 | if (!prev_value)\n 223 | continue;\n 224 | \n 225 | enabled = !g_strcmp0 (prev_value, \"active\");\n 226 | webkit_settings_set_feature_enabled (settings, feature, enabled);\n 227 | gtk_switch_set_active (GTK_SWITCH (switch_widget), enabled);\n 228 | gtk_widget_set_sensitive (button, TRUE);\n 229 | self->non_default_values++;\n 230 | }\n 231 | }\n 232 | \n 233 | gtk_widget_set_sensitive (self->reset_all_row, TRUE);\n 234 | }\n 235 | \n 236 | \n 237 | static void\n 238 | reset_all_row_activated_cb (AdwButtonRow *row,\n 239 | PrefsFeaturesPage *self)\n 240 | {\n 241 | WebKitSettings *settings = ephy_embed_prefs_get_settings ();\n 242 | AdwPreferencesGroup *group;\n 243 | int i = 1;\n 244 | AdwToast *toast;\n 245 | \n 246 | while ((group = adw_preferences_page_get_group (ADW_PREFERENCES_PAGE (self), i++))) {\n 247 | GtkWidget *row;\n 248 | int j = 0;\n 249 | \n 250 | while ((row = adw_preferences_group_get_row (group, j++))) {\n 251 | GtkWidget *switch_widget = adw_action_row_get_activatable_widget (ADW_ACTION_ROW (row));\n 252 | GtkWidget *button = gtk_widget_get_last_child (gtk_widget_get_last_child (gtk_widget_get_first_child (row)));\n 253 | WebKitFeature *feature = g_object_get_data (G_OBJECT (row), \"feature\");\n 254 | gboolean active = gtk_switch_get_active (GTK_SWITCH (switch_widget));\n 255 | gboolean enabled = webkit_feature_get_default_value (feature);\n 256 | \n 257 | if (active == enabled)\n 258 | continue;\n 259 | \n 260 | if (active)\n 261 | g_object_set_data (G_OBJECT (row), \"prev-value\", \"active\");\n 262 | else\n 263 | g_object_set_data (G_OBJECT (row), \"prev-value\", \"inactive\");\n 264 | \n 265 | webkit_settings_set_feature_enabled (settings, feature, enabled);\n 266 | gtk_switch_set_active (GTK_SWITCH (switch_widget), enabled);\n 267 | gtk_widget_set_sensitive (button, FALSE);\n 268 | }\n 269 | }\n 270 | \n 271 | self->non_default_values = 0;\n 272 | gtk_widget_set_sensitive (self->reset_all_row, FALSE);\n 273 | \n 274 | toast = adw_toast_new (_(\"All values reset\"));\n 275 | adw_toast_set_button_label (toast, _(\"_Undo\"));\n 276 | g_signal_connect_object (toast, \"button-clicked\",\n 277 | G_CALLBACK (reset_all_toast_button_clicked_cb), self,\n 278 | G_CONNECT_SWAPPED);\n 279 | adw_preferences_dialog_add_toast (ADW_PREFERENCES_DIALOG (self->dialog), toast);\n 280 | }","lines":77,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/preferences/prefs-features-page.c#L204-280"},{"ref":"src/preferences/prefs-general-page.c:1040-1120","code":" 1040 | static gboolean\n 1041 | default_search_engine_get_mapping (GValue *value,\n 1042 | GVariant *variant,\n 1043 | gpointer user_data)\n 1044 | {\n 1045 | EphySearchEngineManager *manager = ephy_embed_shell_get_search_engine_manager (ephy_embed_shell_get_default ());\n 1046 | const char *text = g_variant_get_string (variant, NULL);\n 1047 | \n 1048 | g_assert (text);\n 1049 | \n 1050 | for (guint i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (manager)); i++) {\n 1051 | EphySearchEngine *engine = g_list_model_get_item (G_LIST_MODEL (manager), i);\n 1052 | \n 1053 | if (g_strcmp0 (ephy_search_engine_get_name (engine), text) == 0) {\n 1054 | g_value_set_uint (value, i);\n 1055 | return TRUE;\n 1056 | }\n 1057 | }\n 1058 | \n 1059 | return FALSE;\n 1060 | }\n 1061 | \n 1062 | static GVariant *\n 1063 | default_search_engine_set_mapping (const GValue *value,\n 1064 | const GVariantType *expected_type,\n 1065 | gpointer user_data)\n 1066 | {\n 1067 | EphySearchEngineManager *manager = ephy_embed_shell_get_search_engine_manager (ephy_embed_shell_get_default ());\n 1068 | EphySearchEngine *engine;\n 1069 | guint i = g_value_get_uint (value);\n 1070 | \n 1071 | if (i >= g_list_model_get_n_items (G_LIST_MODEL (manager)))\n 1072 | return NULL;\n 1073 | \n 1074 | engine = g_list_model_get_item (G_LIST_MODEL (manager), i);\n 1075 | g_assert (engine);\n 1076 | \n 1077 | ephy_search_engine_manager_set_default_engine (manager, engine);\n 1078 | return g_variant_new_string (ephy_search_engine_get_name (engine));\n 1079 | }\n 1080 | \n 1081 | static gboolean\n 1082 | incognito_search_engine_get_mapping (GValue *value,\n 1083 | GVariant *variant,\n 1084 | gpointer user_data)\n 1085 | {\n 1086 | EphySearchEngineManager *manager = ephy_embed_shell_get_search_engine_manager (ephy_embed_shell_get_default ());\n 1087 | const char *text = g_variant_get_string (variant, NULL);\n 1088 | \n 1089 | g_assert (text);\n 1090 | \n 1091 | for (guint i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (manager)); i++) {\n 1092 | EphySearchEngine *engine = g_list_model_get_item (G_LIST_MODEL (manager), i);\n 1093 | \n 1094 | if (g_strcmp0 (ephy_search_engine_get_name (engine), text) == 0) {\n 1095 | g_value_set_uint (value, i);\n 1096 | return TRUE;\n 1097 | }\n 1098 | }\n 1099 | \n 1100 | return FALSE;\n 1101 | }\n 1102 | \n 1103 | static GVariant *\n 1104 | incognito_search_engine_set_mapping (const GValue *value,\n 1105 | const GVariantType *expected_type,\n 1106 | gpointer user_data)\n 1107 | {\n 1108 | EphySearchEngineManager *manager = ephy_embed_shell_get_search_engine_manager (ephy_embed_shell_get_default ());\n 1109 | EphySearchEngine *engine;\n 1110 | guint i = g_value_get_uint (value);\n 1111 | \n 1112 | if (i >= g_list_model_get_n_items (G_LIST_MODEL (manager)))\n 1113 | return NULL;\n 1114 | \n 1115 | engine = g_list_model_get_item (G_LIST_MODEL (manager), i);\n 1116 | g_assert (engine);\n 1117 | \n 1118 | ephy_search_engine_manager_set_incognito_engine (manager, engine);\n 1119 | return g_variant_new_string (ephy_search_engine_get_name (engine));\n 1120 | }","lines":81,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/preferences/prefs-general-page.c#L1040-1120"}]},
{"id":"ARCH_1","type":"architecture","type_label":"Architecture","severity":"low","difficulty":"medium","description":"EphyWebExtensionManager declares a GCancellable *cancellable that is never created (no g_cancellable_new) and never cancelled in dispose. All async file scans / loads pass this NULL cancellable, so in-flight callbacks (on_web_extension_loaded, scan_directory_ready_cb, decompress callbacks) can fire after the manager is disposed, using a freed self. The 'TODO: Cancellable' at the user-message handler reflects the same gap.","fix":"Create the cancellable in constructed and g_cancellable_cancel()+clear it in dispose so pending async operations are aborted.","locations":[{"ref":"src/webextension/ephy-web-extension-manager.c:67","code":" 67 | GCancellable *cancellable;","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-web-extension-manager.c#L67"}]},
{"id":"OTHER_1","type":"other","type_label":"Other","severity":"high","difficulty":"easy","description":"synchronizable_manager_get_collection_name() in ephy-open-tabs-manager.c is missing the `static` qualifier, while the identically-named function in ephy-history-manager.c (and every other synchronizable_manager_* helper) is static. Two non-static functions with the same name in different translation units linked into the same binary is an ODR/external-symbol collision; only the history one being static currently avoids a duplicate-symbol link error. This is an accidental omission.","fix":"Add `static` to the function definition.","locations":[{"ref":"lib/sync/ephy-open-tabs-manager.c:167","code":" 167 | const char *","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-open-tabs-manager.c#L167"}]},
{"id":"OTHER_2","type":"other","type_label":"Other","severity":"high","difficulty":"medium","description":"ephy_web_extension_api_menus_create_context_menu() declares selected_text, is_editable and is_password uninitialized and passes them to parse_context_menu_user_data(), which only assigns each via g_variant_dict_lookup() when the corresponding key exists. For common contexts (no selection / non-editable element) the keys are absent, leaving the variables uninitialized; they are then read unconditionally in create_onclickdata/item_applies_to_context/format_label. This is a page-content-driven uninitialized-memory read.","fix":"Initialize before lookup: `const char *selected_text = NULL; gboolean is_editable = FALSE; gboolean is_password = FALSE;` (and/or set defaults inside parse_context_menu_user_data).","locations":[{"ref":"src/webextension/api/menus.c:688-697","code":" 688 | const char *selected_text;\n 689 | gboolean is_editable;\n 690 | gboolean is_password;\n 691 | GUri *document_uri;\n 692 | GUri *target_uri = NULL;\n 693 | \n 694 | if (!menus)\n 695 | return NULL;\n 696 | \n 697 | parse_context_menu_user_data (context_menu, &selected_text, &is_editable, &is_password);","lines":10,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/menus.c#L688-697"},{"ref":"src/webextension/api/menus.c:429-440","code":" 429 | parse_context_menu_user_data (WebKitContextMenu *context_menu,\n 430 | const char **selected_text,\n 431 | gboolean *is_editable,\n 432 | gboolean *is_password)\n 433 | {\n 434 | GVariantDict dict;\n 435 | \n 436 | g_variant_dict_init (&dict, webkit_context_menu_get_user_data (context_menu));\n 437 | g_variant_dict_lookup (&dict, \"SelectedText\", \"&s\", selected_text);\n 438 | g_variant_dict_lookup (&dict, \"IsEditable\", \"b\", is_editable);\n 439 | g_variant_dict_lookup (&dict, \"IsPassword\", \"b\", is_password);\n 440 | }","lines":12,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/api/menus.c#L429-440"}]},
{"id":"OTHER_3","type":"other","type_label":"Other","severity":"low","difficulty":"easy","description":"ephy_sync_crypto_compate_bytes (note also the misspelled name) does a byte-by-byte non-constant-time comparison that early-returns on the first differing byte, and it is used to verify the response HMAC in derive_master_keys. A non-constant-time MAC comparison is a timing side-channel. (The CBC record HMAC path uses g_strcmp0 which is likewise non-constant-time, and the JWE GCM tag uses memcmp.)","fix":"Use a constant-time comparison (e.g. iterate over the full length accumulating differences with OR, or gnutls/nettle memeql_sec) for all MAC/tag verifications.","locations":[{"ref":"lib/sync/ephy-sync-crypto.c:686-699","code":" 686 | static gboolean\n 687 | ephy_sync_crypto_compate_bytes (const guint8 *a,\n 688 | const guint8 *b,\n 689 | gsize len)\n 690 | {\n 691 | g_assert (a);\n 692 | g_assert (b);\n 693 | \n 694 | for (gsize i = 0; i < len; i++)\n 695 | if (a[i] != b[i])\n 696 | return FALSE;\n 697 | \n 698 | return TRUE;\n 699 | }","lines":14,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-crypto.c#L686-699"},{"ref":"lib/sync/ephy-sync-crypto.c:735","code":" 735 | if (!ephy_sync_crypto_compate_bytes (resp_hmac, resp_hmac_2, len)) {","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/lib/sync/ephy-sync-crypto.c#L735"}]},
{"id":"OTHER_4","type":"other","type_label":"Other","severity":"low","difficulty":"easy","description":"set_badge_color() emits g_object_notify_by_pspec for PROP_BADGE (the badge text property) even though it changes the color, not the text - a misleading change notification, and nothing color-related is ever notified.","fix":"Add a dedicated badge-color property and notify it, or drop the spurious notify.","locations":[{"ref":"src/webextension/ephy-indicator-bin.c:459","code":" 459 | g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BADGE]);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webextension/ephy-indicator-bin.c#L459"}]},
{"id":"OTHER_5","type":"other","type_label":"Other","severity":"negligible","difficulty":"easy","description":"fullscreen_changed_cb is a file-local callback (only referenced via g_signal_connect_object in this file) but is declared with external linkage (missing `static`), unlike every other callback in the file. Pollutes the global symbol namespace.","fix":"Mark fullscreen_changed_cb as static.","locations":[{"ref":"src/ephy-header-bar.c:122","code":" 122 | void","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/ephy-header-bar.c#L122"}]},
{"id":"OTHER_6","type":"other","type_label":"Other","severity":"negligible","difficulty":"easy","description":"ephy_web_app_provider_service_new() builds the application id with g_strconcat (\"org.gnome.Epiphany.WebAppProvider\", NULL), concatenating a single constant string with nothing. This allocates a heap copy for no reason; the literal could be used directly (g_strdup or just the literal).","fix":"Replace with the string literal directly (e.g. pass \"org.gnome.Epiphany.WebAppProvider\" to g_object_new), or use g_strdup if an owned copy is genuinely needed.","locations":[{"ref":"src/webapp-provider/ephy-webapp-provider.c:280","code":" 280 | g_autofree gchar *app_id = g_strconcat (\"org.gnome.Epiphany.WebAppProvider\", NULL);","lines":1,"link":"https://gitlab.gnome.org/GNOME/epiphany/-/blob/a6c5d88c0f722b64deeec1e751a0a269dbdf1071/src/webapp-provider/ephy-webapp-provider.c#L280"}]}
]</script>
<script type="application/json" id="PI">{"name":"epiphany","git_url":"https://gitlab.gnome.org/GNOME/epiphany.git","repo_url":"https://gitlab.gnome.org/GNOME/epiphany","commit":"a6c5d88c0f722b64deeec1e751a0a269dbdf1071","commit_short":"a6c5d88c0f","commit_url":"https://gitlab.gnome.org/GNOME/epiphany/commit/a6c5d88c0f722b64deeec1e751a0a269dbdf1071","branch":"main","generated":"2026-09-09","description":"GNOME Web (codename: Epiphany) is a GNOME web browser based on usually sudden manifestation or perception of the essential nature or meaning of something\" (Merriam-Webster)."}</script>
<div class="app">
<div class="top">
<div>
<h1>Code Audit Report — epiphany</h1>
<div class="meta">Generated 2026-09-09 — <span id="stats"></span></div>
</div>
<div class="top-right">
<div class="search-wrap">
<input type="search" id="search" placeholder="Search findings…" autocomplete="off">
<button id="search-scope" class="action-btn" type="button" title="Toggle: search everywhere vs. only in file/folder paths">📁 Paths only</button>
</div>
<label class="group-label">Group by
<select id="groupby">
<option value="type">Type</option>
<option value="severity">Severity</option>
<option value="file">File</option>
<option value="none">None (flat)</option>
</select>
</label>
</div>
</div>
<div class="project-info"><div class="pi-name">epiphany</div><div class="pi-desc">GNOME Web (codename: Epiphany) is a GNOME web browser based on usually sudden manifestation or perception of the essential nature or meaning of something" (Merriam-Webster).</div><div class="pi-row"><a class="pi-repo-btn" href="https://gitlab.gnome.org/GNOME/epiphany" target="_blank" rel="noopener">🔗 https://gitlab.gnome.org/GNOME/epiphany</a><span><span class="pi-k">branch</span> <code>main</code></span><span><span class="pi-k">commit</span> <a href="https://gitlab.gnome.org/GNOME/epiphany/commit/a6c5d88c0f722b64deeec1e751a0a269dbdf1071" target="_blank" rel="noopener"><code>a6c5d88c0f</code></a></span></div></div>
<div class="filterbar" id="filterbar"></div>
<div id="progress-wrap">
<div id="progress-bar"><div id="progress-fill"></div></div>
<span id="progress-text"></span>
<button class="action-btn" data-action="collapse-all">⊟ Collapse all</button>
<button class="action-btn" data-action="expand-all">⊞ Expand all</button>
<button class="action-btn" data-action="download-md">⬇ MD</button>
<button class="action-btn" data-action="download-md-short">⬇ MD Short</button>
<button class="action-btn" data-action="download-toml">⬇ TOML</button>
</div>
<div id="hidden-bar"></div>
<div id="content"></div>
</div>
<script>
(function(){
'use strict';
// Data
const FINDINGS = JSON.parse(document.getElementById('D').textContent);
const PROJECT_INFO = JSON.parse(document.getElementById('PI').textContent);
const SEVS = ['critical','high','medium','low','negligible'];
const STATUSES = ['open','fixed','wont-fix','false-positive'];
const ALL_TYPES = [...new Set(FINDINGS.map(f => f.type))].sort();
// Build lookup map for O(1) access
const FINDING_BY_ID = new Map(FINDINGS.map(f => [f.id, f]));
// Only severities actually present in the report — the filter bar hides the rest.
const PRESENT_SEVS = SEVS.filter(s => FINDINGS.some(f => (f.severity || 'medium') === s));
// Whether any finding is flagged easy — the "Quick wins" filter row is hidden otherwise.
const DIFFICULTY_ORDER = ['very_easy','easy','medium','hard'];
const DIFFICULTY_LBL = {very_easy:'⚡ Very Easy', easy:'Easy', medium:'Medium', hard:'Hard'};
const DIFFICULTY_CLS = {very_easy:'diff-very-easy', easy:'diff-easy', medium:'diff-medium', hard:'diff-hard'};
const PRESENT_DIFFS = DIFFICULTY_ORDER.filter(d => FINDINGS.some(f => f.difficulty === d));
const HAS_DIFFICULTY = PRESENT_DIFFS.length > 0;
const STORE_KEY = 'audit-status:' + location.href;
const STATUS_CLS = {open:'st-open', fixed:'st-fixed', 'wont-fix':'st-wont', 'false-positive':'st-fp'};
const STATUS_LBL = {open:'● open', fixed:'✓ fixed', 'wont-fix':"— won't fix", 'false-positive':'✗ false pos.'};
const STATUS_NEXT = {open:'fixed', fixed:'wont-fix', 'wont-fix':'false-positive', 'false-positive':'open'};
// State
const S = {
search: '', searchScope: 'all', groupBy: 'type',
hiddenSev: new Set(), hiddenType: new Set(),
hiddenStatus: new Set(), hiddenDiff: new Set(), hiddenGroup: new Set(),
collapsed: new Set(),
};
const statuses = {};
// Persistence
function loadStatuses() {
try { Object.assign(statuses, JSON.parse(localStorage.getItem(STORE_KEY) || '{}')); } catch {}
}
function saveStatuses() {
try { localStorage.setItem(STORE_KEY, JSON.stringify(statuses)); } catch {}
}
function statusOf(id) { return statuses[id] || 'open'; }
// URL hash — debounced so rapid filter changes don't thrash history
let _hashTimer = 0;
function syncHash() {
clearTimeout(_hashTimer);
_hashTimer = setTimeout(() => {
const p = new URLSearchParams();
if (S.groupBy !== 'type') p.set('g', S.groupBy);
if (S.hiddenSev.size) p.set('hs', [...S.hiddenSev].join(','));
if (S.hiddenType.size) p.set('ht', [...S.hiddenType].join(','));
if (S.hiddenStatus.size) p.set('hst', [...S.hiddenStatus].join(','));
if (S.hiddenDiff.size) p.set('hd', [...S.hiddenDiff].join(','));
if (S.search) p.set('q', S.search);
if (S.searchScope !== 'all') p.set('qs', S.searchScope);
const str = p.toString();
history.replaceState(null, '', str ? '#' + str : location.pathname + location.search);
}, 300);
}
function loadHash() {
const p = new URLSearchParams(location.hash.slice(1));
if (p.has('g')) { S.groupBy = p.get('g'); document.getElementById('groupby').value = S.groupBy; }
if (p.has('q')) { S.search = p.get('q'); document.getElementById('search').value = S.search; }
if (p.has('qs')) { S.searchScope = p.get('qs'); }
document.getElementById('search-scope').classList.toggle('active', S.searchScope === 'locations');
if (p.has('hs')) p.get('hs') .split(',').filter(Boolean).forEach(v => S.hiddenSev.add(v));
if (p.has('ht')) p.get('ht') .split(',').filter(Boolean).forEach(v => S.hiddenType.add(v));
if (p.has('hst')) p.get('hst').split(',').filter(Boolean).forEach(v => S.hiddenStatus.add(v));
if (p.has('hd')) p.get('hd') .split(',').filter(Boolean).forEach(v => S.hiddenDiff.add(v));
}
// Helpers
function esc(s) {
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
function highlight(text, q) {
if (!q) return esc(text);
const re = new RegExp('(' + q.replace(/[.*+?^${}()|[\]\\]/g,'\\$&') + ')', 'gi');
return String(text).split(re).map((p, i) => i % 2 ? '<mark>' + esc(p) + '</mark>' : esc(p)).join('');
}
// Render text with ```fenced``` code blocks and `inline` backticks,
// also paragraph-splitting on blank lines. Output is HTML-safe.
function renderMd(text, q) {
const src = String(text == null ? '' : text);
const FENCE = /```[ \t]*([A-Za-z0-9_+-]*)[ \t]*\r?\n([\s\S]*?)```/g;
const parts = [];
let last = 0, m;
while ((m = FENCE.exec(src)) !== null) {
parts.push({kind: 'text', value: src.slice(last, m.index)});
parts.push({kind: 'code', value: m[2].replace(/\r?\n$/, '')});
last = m.index + m[0].length;
}
parts.push({kind: 'text', value: src.slice(last)});
function renderInline(s) {
const re = /`([^`\n]+)`/g;
let out = '', i = 0, mm;
while ((mm = re.exec(s)) !== null) {
out += highlight(s.slice(i, mm.index), q);
out += '<code class="md-inline">' + highlight(mm[1], q) + '</code>';
i = mm.index + mm[0].length;
}
out += highlight(s.slice(i), q);
return out;
}
function renderTextChunk(s) {
const paragraphs = s.split(/\r?\n[ \t]*\r?\n/);
const out = [];
for (const p of paragraphs) {
const trimmed = p.replace(/^\s+|\s+$/g, '');
if (!trimmed) continue;
out.push('<p>' + renderInline(trimmed).replace(/\r?\n/g, '<br>') + '</p>');
}
return out.join('');
}
let html = '';
for (const p of parts) {
if (p.kind === 'text') html += renderTextChunk(p.value);
else html += '<pre class="md-code">' + highlight(p.value, q) + '</pre>';
}
return html;
}
function sevCls(s) { return SEVS.includes(s) ? 'sev-'+s : 'sev-negligible'; }
function sevLbl(s) { return s ? s[0].toUpperCase()+s.slice(1) : 'Medium'; }
function typeLabel(t) { return t.replace(/_/g,' ').replace(/\b\w/g, c => c.toUpperCase()); }
// First sentence of text (up to and including the first period).
function firstSentence(text) {
const m = String(text).match(/\.(\s|$)/);
return m ? text.slice(0, text.indexOf(m[0]) + 1) : text;
}
// Render a "**Label:** value" line. GitHub/CommonMark only recognizes a
// ``` fence when it starts at the beginning of a line, so if the value
// itself opens with a fence, put it on its own line instead of right
// after the inline label - otherwise the fence marker gets glued onto
// "**Label:** ```" and never renders as a code block.
function mdField(label, value) {
value = value || '';
return /^```/.test(value.trim())
? '**' + label + ':**\n\n' + value
: '**' + label + ':** ' + value;
}
// Render the "**Locations:**" block. When a location has a commit-pinned
// permalink (GitHub/GitLab), just paste the bare URL on its own line -
// GitHub/GitLab auto-expand a line-range blob link into an inline code
// preview, so re-pasting the snippet as a fenced block would duplicate it.
// Forges without a detected permalink shape (or missing commit/repo info)
// fall back to the fenced ref+snippet so the location is still readable.
function mdLocations(locations) {
if (!locations.length) return '';
let out = '**Locations:**\n\n';
for (const l of locations) {
if (l.link) out += l.link + '\n\n';
else if (l.code != null) out += '```\n' + l.ref + '\n' + l.code + '\n```\n\n';
else out += '- `' + l.ref + '` _(file not found)_\n\n';
}
return out;
}
// Build full MD report for in-browser download.
function buildMdFull() {
const SORD = ['critical','high','medium','low','negligible'];
const today = new Date().toISOString().slice(0, 10);
let out = '# Code Audit Report\n\n_Generated ' + today + '_\n\n';
const typeMap = new Map();
for (const f of FINDINGS) {
if (!typeMap.has(f.type)) typeMap.set(f.type, []);
typeMap.get(f.type).push(f);
}
const sortedTypes = [...typeMap.keys()].sort();
out += '## Summary\n\n| Type | Severity | Count |\n|------|----------|-------|\n';
for (const t of sortedTypes) {
const items = typeMap.get(t);
const topSev = items.map(f => f.severity || 'medium').reduce((best, s) => {
const bi = SORD.indexOf(best), si = SORD.indexOf(s);
return (si >= 0 && (bi < 0 || si < bi)) ? s : best;
}, 'negligible');
out += '| ' + typeLabel(t) + ' | ' + topSev + ' | ' + items.length + ' |\n';
}
for (const t of sortedTypes) {
out += '\n## ' + typeLabel(t) + '\n\n';
for (const f of typeMap.get(t)) {
const sevStr = f.severity ? ' `' + f.severity.toUpperCase() + '`' : '';
out += '### ' + f.id + sevStr + '\n\n';
out += mdField('Description', f.description) + '\n\n';
out += mdLocations(f.locations);
out += mdField('Fix', f.fix) + '\n\n---\n\n';
}
}
return out;
}
// Build MD short checklist for in-browser download.
function buildMdShort() {
const lines = [];
const ids = [];
const sorted = [...FINDINGS].sort((a, b) => a.type.localeCompare(b.type));
for (const f of sorted) {
ids.push(f.id);
const desc = firstSentence(f.description || '');
const locs = f.locations.map(l => l.ref);
const locStr = locs.length ? ' - [' + locs.join(', ') + ']' : '';
const sevStr = f.severity ? ' [' + f.severity.toUpperCase() + ']' : '';
lines.push('- [ ] - ' + f.id + sevStr + ' - ' + desc + locStr);
}
lines.push('', '## IDs only', '');
ids.forEach(id => lines.push('- [ ] - ' + id));
return lines.join('\n') + '\n';
}
function downloadText(text, filename, mime) {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([text], {type: mime || 'text/plain'}));
a.download = filename;
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
}
function tomlStr(s) {
return '"' + String(s == null ? '' : s)
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t') + '"';
}
// Build a [project] header block from embedded PROJECT_INFO, so the exported
// findings.toml is self-contained (doesn't depend on a sibling project-info.toml
// that a second, separately-triggered download could silently fail to save).
// '' when there is no project info at all.
function buildProjectToml() {
const ORDER = ['name','description','git_url','repo_url','commit','commit_short','commit_url','branch','generated'];
const pi = PROJECT_INFO || {};
if (!Object.keys(pi).length) return '';
const lines = ['[project]'];
for (const k of ORDER) {
if (k in pi) lines.push(k + ' = ' + tomlStr(pi[k]));
}
return lines.join('\n') + '\n\n';
}
// Build TOML matching the original findings.toml format (re-parseable by toml_to_report.py).
function buildFindingsToml() {
function pad(key) { return (key + ' ').slice(0, 12); }
const chunks = [];
for (const f of FINDINGS) {
const lns = ['[[finding]]'];
lns.push(pad('type') + '= ' + tomlStr(f.type));
lns.push(pad('severity') + '= ' + tomlStr(f.severity || 'medium'));
if (f.difficulty) lns.push(pad('difficulty') + '= ' + tomlStr(f.difficulty));
lns.push(pad('description') + '= ' + tomlStr(f.description || ''));
const refs = f.locations.map(l => tomlStr(l.ref));
lns.push(pad('locations') + '= [' + refs.join(', ') + ']');
lns.push(pad('fix') + '= ' + tomlStr(f.fix || ''));
chunks.push(lns.join('\n'));
}
return buildProjectToml() + chunks.join('\n\n') + '\n';
}
// Build the GitHub-ready Markdown for one finding (mirrors findings.md).
function findingToMd(f) {
let out = '### ' + f.id + (f.severity ? ' `' + f.severity.toUpperCase() + '`' : '') + '\n\n';
out += mdField('Description', f.description) + '\n\n';
out += mdLocations(f.locations);
out += mdField('Fix', f.fix) + '\n';
return out;
}
function flashBtn(btn, msg) {
btn.textContent = msg;
setTimeout(() => { btn.textContent = '⧉ Copy'; }, 1200);
}
function copyText(text, btn) {
const ok = () => flashBtn(btn, '✓ Copied');
const no = () => flashBtn(btn, '✗ Failed');
const fallback = () => {
try {
const ta = document.createElement('textarea');
ta.value = text; ta.style.position = 'fixed'; ta.style.top = '-9999px';
document.body.appendChild(ta); ta.select();
const r = document.execCommand('copy');
document.body.removeChild(ta);
r ? ok() : no();
} catch { no(); }
};
if (navigator.clipboard && navigator.clipboard.writeText)
navigator.clipboard.writeText(text).then(ok, fallback);
else fallback();
}
// Filter logic
function getKey(f) {
if (S.groupBy === 'severity') return f.severity || 'medium';
if (S.groupBy === 'file') return f.locations.length ? f.locations[0].ref.replace(/:.*/,'') : '(no location)';
if (S.groupBy === 'none') return '';
return f.type_label;
}
// Pre-compute search haystacks per finding (cleared on full re-render).
// Two variants: everything (default), and file/folder paths only - for when a
// search term (e.g. a directory name like 'dlls/abc') also happens to appear
// inside description/fix prose and you only want path matches.
const _hay = new Map();
const _hayLoc = new Map();
function hayOf(f) {
if (!_hay.has(f.id)) {
_hay.set(f.id, [f.id, f.type_label, f.severity, f.description, f.fix,
f.difficulty ? (DIFFICULTY_LBL[f.difficulty] || f.difficulty) : '',
...f.locations.map(l => l.ref)].join(' ').toLowerCase());
}
return _hay.get(f.id);
}
function hayLocOf(f) {
if (!_hayLoc.has(f.id)) {
_hayLoc.set(f.id, f.locations.map(l => l.ref).join(' ').toLowerCase());
}
return _hayLoc.get(f.id);
}
function matches(f) {
if (S.hiddenDiff.size > 0 && f.difficulty && S.hiddenDiff.has(f.difficulty)) return false;
if (S.hiddenSev.has(f.severity || 'medium')) return false;
if (S.hiddenType.has(f.type)) return false;
const st = statusOf(f.id);
if (S.hiddenStatus.has(st)) return false;
if (S.search) {
const q = S.search.toLowerCase();
if (S.searchScope === 'locations') {
if (!hayLocOf(f).includes(q)) return false;
} else if (!hayOf(f).includes(q) && !st.includes(q)) {
return false;
}
}
return true;
}
function sortKeys(keys) {
if (S.groupBy === 'severity') {
return [...keys].sort((a, b) => {
const ai = SEVS.indexOf(a), bi = SEVS.indexOf(b);
return (ai < 0 ? 99 : ai) - (bi < 0 ? 99 : bi);
});
}
return [...keys].sort();
}
// Build body HTML for an expanded card (called lazily on first expand).
function renderCardBody(f, q) {
let locs = '';
for (const l of f.locations) {
const lineInfo = l.lines != null
? ' <span class="loc-lines">(' + l.lines + ' line' + (l.lines === 1 ? '' : 's') + ')</span>'
: '';
const refHtml = l.link
? '<a class="loc-link" href="' + esc(l.link) + '" target="_blank" rel="noopener" title="Open at this commit">' + highlight(l.ref, q) + ' ↗</a>'
: highlight(l.ref, q);
locs += '<div class="loc-block"><div class="loc-ref">' + refHtml + lineInfo + '</div>';
locs += l.code != null ? '<pre>' + esc(l.code) + '</pre>' : '<div class="not-found">(file not found)</div>';
locs += '</div>';
}
return (
'<div class="field-lbl">Description</div>' +
'<div class="desc">' + renderMd(f.description, q) + '</div>' +
(locs ? '<div class="field-lbl">Locations</div>' + locs : '') +
'<div class="field-lbl">Fix</div>' +
'<div class="fix-txt">' + renderMd(f.fix, q) + '</div>'
);
}
// Render card shell only — header + empty or populated body.
// When collapsed the body is empty (display:none) — avoids renderMd for 1800+ cards.
function renderCard(f, q) {
const sc = sevCls(f.severity || 'medium');
const sl = sevLbl(f.severity || 'medium');
const st = statusOf(f.id);
const sc2 = STATUS_CLS[st] || 'st-open';
const sl2 = STATUS_LBL[st] || '● open';
const isCollapsed = S.collapsed.has(f.id);
const isResolved = st !== 'open';
const locSummary = f.locations.length
? f.locations[0].ref + (f.locations.length > 1 ? ' \u2026' : '')
: '';
const bodyInner = isCollapsed ? '' : renderCardBody(f, q);
return (
'<div class="card' + (isResolved ? ' resolved' : '') + '" data-card-id="' + esc(f.id) + '">' +
'<div class="card-header" data-action="toggle-card" data-id="' + esc(f.id) + '">' +
'<span class="card-arrow">' + (isCollapsed ? '\u25b6' : '\u25bc') + '</span>' +
'<span class="card-id">' + esc(f.id) + '</span>' +
'<span class="badge ' + sc + '" style="cursor:default">' + esc(sl) + '</span>' +
'<span class="badge type-badge" style="cursor:default">' + esc(f.type_label) + '</span>' +
(f.difficulty ? '<span class="badge ' + (DIFFICULTY_CLS[f.difficulty]||'') + '" style="cursor:default" title="Difficulty: ' + esc(DIFFICULTY_LBL[f.difficulty]||f.difficulty) + '">' + esc(DIFFICULTY_LBL[f.difficulty]||f.difficulty) + '</span>' : '') +
(locSummary ? '<span class="card-loc" title="' + esc(locSummary) + '">' + esc(locSummary) + '</span>' : '') +
'<button class="status-btn ' + sc2 + '" data-action="cycle-status" data-id="' + esc(f.id) + '">' + sl2 + '</button>' +
'<button class="copy-btn" data-action="copy-md" data-id="' + esc(f.id) + '" title="Copy as Markdown for GitHub">\u29c9 Copy</button>' +
'</div>' +
'<div class="card-body" data-body-id="' + esc(f.id) + '"' + (isCollapsed ? ' style="display:none"' : '') + '>' +
bodyInner +
'</div></div>'
);
}
function initFilterBar() {
const fb = document.getElementById('filterbar');
let h = '<div class="filter-row"><span class="filter-lbl">Severity:</span>';
for (const s of PRESENT_SEVS)
h += '<button class="badge ' + sevCls(s) + '" data-sev="' + s + '">' + sevLbl(s) + '</button>';
h += '<button class="filter-toggle" data-toggle-all="sev"></button></div>';
h += '<div class="filter-row"><span class="filter-lbl">Type:</span>';
for (const t of ALL_TYPES)
h += '<button class="badge type-badge" data-type="' + esc(t) + '">' + esc(typeLabel(t)) + '</button>';
h += '<button class="filter-toggle" data-toggle-all="type"></button></div>';
h += '<div class="filter-row"><span class="filter-lbl">Status:</span>';
for (const st of STATUSES)
h += '<button class="badge ' + (STATUS_CLS[st]||'st-open') + '" data-status="' + st + '">' + (STATUS_LBL[st]||st) + '</button>';
h += '<button class="filter-toggle" data-toggle-all="status"></button></div>';
if (HAS_DIFFICULTY) {
h += '<div class="filter-row"><span class="filter-lbl">Difficulty:</span>';
for (const d of PRESENT_DIFFS)
h += '<button class="badge ' + (DIFFICULTY_CLS[d]||'') + '" data-diff="' + d + '">' + esc(DIFFICULTY_LBL[d]||d) + '</button>';
h += '<button class="filter-toggle" data-toggle-all="diff"></button></div>';
}
fb.innerHTML = h;
}
function updateToggles() {
for (const b of document.querySelectorAll('[data-sev]')) b.classList.toggle('off', S.hiddenSev.has(b.dataset.sev));
for (const b of document.querySelectorAll('[data-type]')) b.classList.toggle('off', S.hiddenType.has(b.dataset.type));
for (const b of document.querySelectorAll('[data-status]')) b.classList.toggle('off', S.hiddenStatus.has(b.dataset.status));
for (const b of document.querySelectorAll('[data-diff]')) b.classList.toggle('off', S.hiddenDiff.has(b.dataset.diff));
for (const b of document.querySelectorAll('[data-toggle-all]')) {
const k = b.dataset.toggleAll;
const set = k === 'sev' ? S.hiddenSev : k === 'type' ? S.hiddenType : k === 'status' ? S.hiddenStatus : k === 'diff' ? S.hiddenDiff : null;
if (set) b.textContent = set.size === 0 ? 'Hide all' : 'Show all';
}
}
function updateProgress() {
const total = FINDINGS.length;
const done = FINDINGS.filter(f => statusOf(f.id) !== 'open').length;
const pct = total ? Math.round(done / total * 100) : 0;
document.getElementById('progress-fill').style.width = pct + '%';
document.getElementById('progress-text').textContent = done + ' / ' + total + ' resolved (' + pct + '%)';
}
// Surgical toggle: flip one card without touching the rest of the DOM.
function toggleCard(id) {
const wasCollapsed = S.collapsed.has(id);
if (wasCollapsed) S.collapsed.delete(id); else S.collapsed.add(id);
const body = document.querySelector('[data-body-id="' + id + '"]');
const arrow = document.querySelector('[data-card-id="' + id + '"] .card-arrow');
if (!body) { render(); return; }
if (wasCollapsed) {
if (!body.firstChild) {
const f = FINDING_BY_ID.get(id);
if (f) body.innerHTML = renderCardBody(f, S.search);
}
body.style.display = '';
if (arrow) arrow.textContent = '\u25bc';
} else {
body.style.display = 'none';
if (arrow) arrow.textContent = '\u25b6';
}
}
// Surgical status update: update one card's button + resolved class.
function cycleStatus(id) {
statuses[id] = STATUS_NEXT[statusOf(id)] || 'open';
saveStatuses();
const st = statusOf(id);
const btn = document.querySelector('.status-btn[data-id="' + id + '"]');
if (!btn) { render(); return; }
btn.className = 'status-btn ' + (STATUS_CLS[st] || 'st-open');
btn.textContent = STATUS_LBL[st] || '\u25cf open';
const card = document.querySelector('[data-card-id="' + id + '"]');
if (card) card.classList.toggle('resolved', st !== 'open');
updateProgress();
}
// Surgical collapse/expand for a group: toggle all bodies in the group without full re-render.
function collapseGroup(key, collapse) {
FINDINGS.forEach(f => {
if (getKey(f) !== key) return;
if (collapse) S.collapsed.add(f.id); else S.collapsed.delete(f.id);
const body = document.querySelector('[data-body-id="' + f.id + '"]');
const arrow = document.querySelector('[data-card-id="' + f.id + '"] .card-arrow');
if (!body) return;
if (collapse) {
body.style.display = 'none';
if (arrow) arrow.textContent = '\u25b6';
} else {
if (!body.firstChild) {
const ff = FINDING_BY_ID.get(f.id);
if (ff) body.innerHTML = renderCardBody(ff, S.search);
}
body.style.display = '';
if (arrow) arrow.textContent = '\u25bc';
}
});
}
// Full re-render: used for filter/group-by/search changes.
// Batched via requestAnimationFrame.
let _rafPending = false;
function scheduleRender() {
if (_rafPending) return;
_rafPending = true;
requestAnimationFrame(() => { _rafPending = false; render(); });
}
function render() {
_hay.clear();
_hayLoc.clear();
updateToggles();
updateProgress();
const q = S.search;
const visible = FINDINGS.filter(matches);
document.getElementById('stats').textContent = visible.length + ' / ' + FINDINGS.length + ' findings';
// Build groups
const map = new Map();
for (const f of visible) {
const k = getKey(f);
if (!map.has(k)) map.set(k, []);
map.get(k).push(f);
}
// Hidden-groups bar
const hbar = document.getElementById('hidden-bar');
if (S.hiddenGroup.size > 0) {
let hh = '<div class="hidden-inner"><span class="hidden-lbl">Hidden:</span>';
for (const g of [...S.hiddenGroup].sort())
hh += '<button class="show-btn" data-action="show-group" data-key="' + esc(g) + '">' + esc(g || '(all findings)') + '</button>';
hh += '<button class="show-all-btn" data-action="show-all">Show all</button></div>';
hbar.innerHTML = hh;
} else {
hbar.innerHTML = '';
}
// Content
const content = document.getElementById('content');
if (!visible.length) {
content.innerHTML = '<div class="no-results">No findings match the current filters.</div>';
syncHash();
return;
}
let hh = '';
for (const k of sortKeys([...map.keys()])) {
if (S.hiddenGroup.has(k)) continue;
const items = map.get(k);
const title = k || 'All Findings';
const done = items.filter(f => statusOf(f.id) !== 'open').length;
const allDone = done === items.length;
hh += '<div class="group-section">' +
'<div class="group-header">' +
'<span class="group-title' + (allDone ? ' all-done' : '') + '">' + esc(title) + '</span>' +
'<span class="group-count">' + done + '/' + items.length + '</span>' +
'<button class="icon-btn" data-action="collapse-group" data-key="' + esc(k) + '" title="Collapse all">\u229f</button>' +
'<button class="icon-btn" data-action="expand-group" data-key="' + esc(k) + '" title="Expand all">\u229e</button>' +
'<button class="hide-btn" data-action="hide-group" data-key="' + esc(k) + '">Hide</button>' +
'</div>';
for (const f of items) hh += renderCard(f, q);
hh += '</div>';
}
content.innerHTML = hh;
syncHash();
}
// Long-press solo: hold a filter badge 450 ms to isolate that value (hide all others in the group).
let _soloTimer = 0;
let _soloFired = false;
function _applySolo(b) {
_soloFired = true;
const sev = b.dataset.sev, typ = b.dataset.type, sta = b.dataset.status, dif = b.dataset.diff;
if (sev) { S.hiddenSev.clear(); PRESENT_SEVS.filter(v => v !== sev).forEach(v => S.hiddenSev.add(v)); }
else if (typ) { S.hiddenType.clear(); ALL_TYPES.filter(v => v !== typ).forEach(v => S.hiddenType.add(v)); }
else if (sta) { S.hiddenStatus.clear(); STATUSES.filter(v => v !== sta).forEach(v => S.hiddenStatus.add(v)); }
else if (dif) { S.hiddenDiff.clear(); PRESENT_DIFFS.filter(v => v !== dif).forEach(v => S.hiddenDiff.add(v)); }
scheduleRender();
}
const _fb = document.getElementById('filterbar');
_fb.addEventListener('pointerdown', e => {
const b = e.target.closest('[data-sev],[data-type],[data-status],[data-diff]');
if (!b) return;
_soloFired = false;
clearTimeout(_soloTimer);
_soloTimer = setTimeout(() => _applySolo(b), 450);
}, {passive: true});
_fb.addEventListener('pointerup', () => clearTimeout(_soloTimer), {passive: true});
_fb.addEventListener('pointercancel', () => clearTimeout(_soloTimer), {passive: true});
_fb.addEventListener('pointermove', e => {
if (!_soloTimer) return;
if (!e.target.closest('[data-sev],[data-type],[data-status],[data-diff]')) clearTimeout(_soloTimer);
}, {passive: true});
// Event delegation
_fb.addEventListener('click', e => {
if (_soloFired) { _soloFired = false; return; }
const b = e.target.closest('[data-sev],[data-type],[data-status],[data-toggle-all],[data-diff]');
if (!b) return;
if (b.dataset.toggleAll) {
const k = b.dataset.toggleAll;
const set = k === 'sev' ? S.hiddenSev : k === 'type' ? S.hiddenType : k === 'status' ? S.hiddenStatus : k === 'diff' ? S.hiddenDiff : null;
const full = k === 'sev' ? PRESENT_SEVS : k === 'type' ? ALL_TYPES : k === 'status' ? STATUSES : k === 'diff' ? PRESENT_DIFFS : [];
if (!set) return;
if (set.size === 0) full.forEach(v => set.add(v));
else set.clear();
}
else if (b.dataset.sev) { const s = S.hiddenSev; s.has(b.dataset.sev) ? s.delete(b.dataset.sev) : s.add(b.dataset.sev); }
else if (b.dataset.type) { const s = S.hiddenType; s.has(b.dataset.type) ? s.delete(b.dataset.type) : s.add(b.dataset.type); }
else if (b.dataset.status) { const s = S.hiddenStatus; s.has(b.dataset.status) ? s.delete(b.dataset.status) : s.add(b.dataset.status); }
else if (b.dataset.diff) { const s = S.hiddenDiff; s.has(b.dataset.diff) ? s.delete(b.dataset.diff) : s.add(b.dataset.diff); }
scheduleRender();
});
document.addEventListener('click', e => {