View Issue Details

IDProjectCategoryView StatusLast Update
0035603FPCCompilerpublic2019-12-10 21:10
ReporterVille Krumlinde Assigned ToFlorian  
PrioritynormalSeverityminorReproducibilityalways
Status resolvedResolutionduplicate 
Product Version3.3.1 
Summary0035603: Case statement does not handle out of bounds value of enumerated type, crashes instead
DescriptionSee attached files.

Case statement has generated jump table for enumerated type but does not test that value is in the enumeration, so it reads random address and jumps to it, leading to crash.

Excepted result is that it should jump to "else" statement.

This is in latest trunk build. It was working in Fpc build 3-6 months ago.

In the attached generated asm the case is compiled to this:
# [20] case global1 of
    andl $255,%eax
    leaq Ld1(%rip),%rdx
    movslq (%rdx,%rax,4),%rax
    addq %rdx,%rax
    jmp *%rax

There is no check that the value is in range of the jump table.
Steps To ReproduceBuild attached file and run it. It will crash.
TagsNo tags attached.
Fixed in Revision
FPCOldBugId
FPCTarget-
Attached Files

Relationships

duplicate of 0032079 resolvedJonas Maebe Jumptables for case..of may jump into invalid memory 

Activities

Ville Krumlinde

2019-05-20 10:17

reporter  

project1.lpr (1,236 bytes)   
program project1;

type
  TProblem = (
    BC_Square,
    BC_Rounded_Light, BC_Rounded_Medium, BC_Rounded_Full, BC_Rounded_Full_MoreRoom, BC_Rounded_Full_H, BC_Rounded_Full_V, BC_Rounded_Rel_Light,
    BC_Balloon_Light, BC_Balloon_Medium, BC_Balloon_Full, BC_CRT,
    BC_Pad_Soft, BC_Pad_Light, BC_Pad_Medium, BC_Pad_Full, BC_Pad_Cut,
    BC_Bulge_H, BC_Bulge_V, BC_Drop_H, BC_Drop_V, BC_Bean, BC_Bean_H, BC_Bean_V, BC_Bean_Alt_H, BC_Cushion_Light_H, BC_Cushion_Medium_H,
    BC_Drop_In_H,
    BC_Arrow_In_H, BC_Arrow_In_V, BC_Arrow_Out_H, BC_Arrow_Out_V,
    BC_Skew_H, BC_Skew_V, BC_AltSkew_H, BC_AltSkew_V, BC_SharpSkew_H, BC_SharpSkew_V
    );

var
  global1 : TProblem;
begin
  global1 := TProblem(-1);

  case global1 of
    BC_Rounded_Full: writeln('ok');
    BC_Rounded_Full_MoreRoom: writeln('ok');
    BC_Drop_H: writeln('ok');
    BC_Drop_V: writeln('ok');
    BC_Drop_In_H: writeln('ok');
    BC_Arrow_In_H: writeln('ok');
    BC_Arrow_In_V: writeln('ok');
    BC_Arrow_Out_H, BC_Rounded_Full_H: writeln('ok');
    BC_Arrow_Out_V, BC_Rounded_Full_V: writeln('ok');
    BC_Skew_H, BC_AltSkew_H, BC_SharpSkew_H: writeln('ok');
    BC_Skew_V, BC_AltSkew_V, BC_SharpSkew_V: writeln('ok');
  else
    writeln('ok');
  end;
end.

project1.lpr (1,236 bytes)   
project1.s (6,794 bytes)   
# Begin asmlist al_procedures

.text
	.align 4
.globl	_main
_main:
	leaq	-8(%rsp),%rsp
# Var ARGC located in register eax
# Var ARGV located in register rsi
# Var ARGP located in register rdx
# Var ARGP located in register rdx
# Var ARGV located in register rsi
# Var ARGC located in register edi
	call	_FPC_SYSTEMMAIN
	leaq	8(%rsp),%rsp
	ret

.text
	.align 4
.globl	_PASCALMAIN
_PASCALMAIN:
# [project1.lpr]
# [17] begin
	pushq	%rbx
	call	fpc_initializeunits
# [18] global1 := TProblem(-1);
	movb	$255,%al
	movb	%al,_U_$P$PROJECT1_$$_GLOBAL1(%rip)
# [20] case global1 of
	andl	$255,%eax
	leaq	Ld1(%rip),%rdx
	movslq	(%rdx,%rax,4),%rax
	addq	%rdx,%rax
	jmp	*%rax
	.align 4
Lj9:
# [21] BC_Rounded_Full: writeln('ok');
	call	fpc_get_output
	movq	%rax,%rbx
	movq	_$PROJECT1$_Ld2@GOTPCREL(%rip),%rdx
	movq	%rbx,%rsi
	xorl	%edi,%edi
	call	fpc_write_text_shortstr
	call	fpc_iocheck
	movq	%rbx,%rdi
	call	fpc_writeln_end
	call	fpc_iocheck
	jmp	Lj7
	.align 4
Lj10:
# [22] BC_Rounded_Full_MoreRoom: writeln('ok');
	call	fpc_get_output
	movq	%rax,%rbx
	movq	_$PROJECT1$_Ld2@GOTPCREL(%rip),%rdx
	movq	%rbx,%rsi
	xorl	%edi,%edi
	call	fpc_write_text_shortstr
	call	fpc_iocheck
	movq	%rbx,%rdi
	call	fpc_writeln_end
	call	fpc_iocheck
	jmp	Lj7
	.align 4
Lj11:
# [23] BC_Drop_H: writeln('ok');
	call	fpc_get_output
	movq	%rax,%rbx
	movq	_$PROJECT1$_Ld2@GOTPCREL(%rip),%rdx
	movq	%rbx,%rsi
	xorl	%edi,%edi
	call	fpc_write_text_shortstr
	call	fpc_iocheck
	movq	%rbx,%rdi
	call	fpc_writeln_end
	call	fpc_iocheck
	jmp	Lj7
	.align 4
Lj12:
# [24] BC_Drop_V: writeln('ok');
	call	fpc_get_output
	movq	%rax,%rbx
	movq	_$PROJECT1$_Ld2@GOTPCREL(%rip),%rdx
	movq	%rbx,%rsi
	xorl	%edi,%edi
	call	fpc_write_text_shortstr
	call	fpc_iocheck
	movq	%rbx,%rdi
	call	fpc_writeln_end
	call	fpc_iocheck
	jmp	Lj7
	.align 4
Lj13:
# [25] BC_Drop_In_H: writeln('ok');
	call	fpc_get_output
	movq	%rax,%rbx
	movq	_$PROJECT1$_Ld2@GOTPCREL(%rip),%rdx
	movq	%rbx,%rsi
	xorl	%edi,%edi
	call	fpc_write_text_shortstr
	call	fpc_iocheck
	movq	%rbx,%rdi
	call	fpc_writeln_end
	call	fpc_iocheck
	jmp	Lj7
	.align 4
Lj14:
# [26] BC_Arrow_In_H: writeln('ok');
	call	fpc_get_output
	movq	%rax,%rbx
	movq	_$PROJECT1$_Ld2@GOTPCREL(%rip),%rdx
	movq	%rbx,%rsi
	xorl	%edi,%edi
	call	fpc_write_text_shortstr
	call	fpc_iocheck
	movq	%rbx,%rdi
	call	fpc_writeln_end
	call	fpc_iocheck
	jmp	Lj7
	.align 4
Lj15:
# [27] BC_Arrow_In_V: writeln('ok');
	call	fpc_get_output
	movq	%rax,%rbx
	movq	_$PROJECT1$_Ld2@GOTPCREL(%rip),%rdx
	movq	%rbx,%rsi
	xorl	%edi,%edi
	call	fpc_write_text_shortstr
	call	fpc_iocheck
	movq	%rbx,%rdi
	call	fpc_writeln_end
	call	fpc_iocheck
	jmp	Lj7
	.align 4
Lj16:
# [28] BC_Arrow_Out_H, BC_Rounded_Full_H: writeln('ok');
	call	fpc_get_output
	movq	%rax,%rbx
	movq	_$PROJECT1$_Ld2@GOTPCREL(%rip),%rdx
	movq	%rbx,%rsi
	xorl	%edi,%edi
	call	fpc_write_text_shortstr
	call	fpc_iocheck
	movq	%rbx,%rdi
	call	fpc_writeln_end
	call	fpc_iocheck
	jmp	Lj7
	.align 4
Lj17:
# [29] BC_Arrow_Out_V, BC_Rounded_Full_V: writeln('ok');
	call	fpc_get_output
	movq	%rax,%rbx
	movq	_$PROJECT1$_Ld2@GOTPCREL(%rip),%rdx
	movq	%rbx,%rsi
	xorl	%edi,%edi
	call	fpc_write_text_shortstr
	call	fpc_iocheck
	movq	%rbx,%rdi
	call	fpc_writeln_end
	call	fpc_iocheck
	jmp	Lj7
	.align 4
Lj18:
# [30] BC_Skew_H, BC_AltSkew_H, BC_SharpSkew_H: writeln('ok');
	call	fpc_get_output
	movq	%rax,%rbx
	movq	_$PROJECT1$_Ld2@GOTPCREL(%rip),%rdx
	movq	%rbx,%rsi
	xorl	%edi,%edi
	call	fpc_write_text_shortstr
	call	fpc_iocheck
	movq	%rbx,%rdi
	call	fpc_writeln_end
	call	fpc_iocheck
	jmp	Lj7
	.align 4
Lj19:
# [31] BC_Skew_V, BC_AltSkew_V, BC_SharpSkew_V: writeln('ok');
	call	fpc_get_output
	movq	%rax,%rbx
	movq	_$PROJECT1$_Ld2@GOTPCREL(%rip),%rdx
	movq	%rbx,%rsi
	xorl	%edi,%edi
	call	fpc_write_text_shortstr
	call	fpc_iocheck
	movq	%rbx,%rdi
	call	fpc_writeln_end
	call	fpc_iocheck
	jmp	Lj7
	.align 4
Lj8:
# [33] writeln('ok');
	call	fpc_get_output
	movq	%rax,%rbx
	movq	_$PROJECT1$_Ld2@GOTPCREL(%rip),%rdx
	movq	%rbx,%rsi
	xorl	%edi,%edi
	call	fpc_write_text_shortstr
	call	fpc_iocheck
	movq	%rbx,%rdi
	call	fpc_writeln_end
	call	fpc_iocheck
	.align 4
Lj7:
# [35] end.
	call	fpc_do_exit
	popq	%rbx
	ret

.text
	.align 2
Ld1:
	.long	L$set$1
	.set L$set$1,Lj8-Ld1
	.long	L$set$2
	.set L$set$2,Lj8-Ld1
	.long	L$set$3
	.set L$set$3,Lj8-Ld1
	.long	L$set$4
	.set L$set$4,Lj9-Ld1
	.long	L$set$5
	.set L$set$5,Lj10-Ld1
	.long	L$set$6
	.set L$set$6,Lj16-Ld1
	.long	L$set$7
	.set L$set$7,Lj17-Ld1
	.long	L$set$8
	.set L$set$8,Lj8-Ld1
	.long	L$set$9
	.set L$set$9,Lj8-Ld1
	.long	L$set$10
	.set L$set$10,Lj8-Ld1
	.long	L$set$11
	.set L$set$11,Lj8-Ld1
	.long	L$set$12
	.set L$set$12,Lj8-Ld1
	.long	L$set$13
	.set L$set$13,Lj8-Ld1
	.long	L$set$14
	.set L$set$14,Lj8-Ld1
	.long	L$set$15
	.set L$set$15,Lj8-Ld1
	.long	L$set$16
	.set L$set$16,Lj8-Ld1
	.long	L$set$17
	.set L$set$17,Lj8-Ld1
	.long	L$set$18
	.set L$set$18,Lj8-Ld1
	.long	L$set$19
	.set L$set$19,Lj8-Ld1
	.long	L$set$20
	.set L$set$20,Lj11-Ld1
	.long	L$set$21
	.set L$set$21,Lj12-Ld1
	.long	L$set$22
	.set L$set$22,Lj8-Ld1
	.long	L$set$23
	.set L$set$23,Lj8-Ld1
	.long	L$set$24
	.set L$set$24,Lj8-Ld1
	.long	L$set$25
	.set L$set$25,Lj8-Ld1
	.long	L$set$26
	.set L$set$26,Lj8-Ld1
	.long	L$set$27
	.set L$set$27,Lj8-Ld1
	.long	L$set$28
	.set L$set$28,Lj13-Ld1
	.long	L$set$29
	.set L$set$29,Lj14-Ld1
	.long	L$set$30
	.set L$set$30,Lj15-Ld1
	.long	L$set$31
	.set L$set$31,Lj16-Ld1
	.long	L$set$32
	.set L$set$32,Lj17-Ld1
	.long	L$set$33
	.set L$set$33,Lj18-Ld1
	.long	L$set$34
	.set L$set$34,Lj19-Ld1
	.long	L$set$35
	.set L$set$35,Lj18-Ld1
	.long	L$set$36
	.set L$set$36,Lj19-Ld1
	.long	L$set$37
	.set L$set$37,Lj18-Ld1
	.long	L$set$38
	.set L$set$38,Lj19-Ld1
# End asmlist al_procedures
# Begin asmlist al_globals


# [16] global1 : TProblem;
	.lcomm	_U_$P$PROJECT1_$$_GLOBAL1,1,1

.data
	.align 3
.globl	INITFINAL
INITFINAL:
	.quad	2,0
	.quad	_INIT$_$SYSTEM
	.quad	0,0
	.quad	_FINALIZE$_$OBJPAS

.data
	.align 3
.globl	FPC_THREADVARTABLES
FPC_THREADVARTABLES:
	.long	1
	.quad	_THREADVARLIST_$SYSTEM$indirect

.const_data
	.align 3
.globl	FPC_RESOURCESTRINGTABLES
FPC_RESOURCESTRINGTABLES:
	.quad	0

.data
	.align 3
.globl	FPC_WIDEINITTABLES
FPC_WIDEINITTABLES:
	.quad	0

.data
	.align 3
.globl	FPC_RESSTRINITTABLES
FPC_RESSTRINITTABLES:
	.quad	0

.section __TEXT, .fpc, regular, no_dead_strip
	.align 4
.reference __fpc_ident
__fpc_ident:
	.ascii	"FPC 3.3.1 [2019/05/20] for x86_64 - Darwin"

.data
	.align 3
.globl	__stklen
__stklen:
	.quad	262144

.data
	.align 3
.globl	__heapsize
__heapsize:
	.quad	0

.data
	.align 3
.globl	__fpc_valgrind
__fpc_valgrind:
	.byte	0

.const_data
	.align 3
.globl	FPC_RESLOCATION
FPC_RESLOCATION:
	.quad	0
# End asmlist al_globals
# Begin asmlist al_typedconsts

.const
	.align 3
.globl	_$PROJECT1$_Ld2
_$PROJECT1$_Ld2:
	.ascii	"\002ok\000"
# End asmlist al_typedconsts
	.subsections_via_symbols

project1.s (6,794 bytes)   

Marģers

2019-05-20 13:24

reporter   ~0116281

As in global1 is typecast none existent enumeration value, taking else statement also is wrong. Case statement covers all legal values, then range check is not necessary.
Some time ago I reported similar issue about arrays and resolution was "no changes required".
Program crash is bad, but only reasonable "solution". It is program error, not compiler error.

J. Gareth Moreton

2019-05-20 13:25

developer   ~0116282

When evaluating case blocks, the compiler assumes that the input value is within the type's domain - it makes no bounds check. This is a conscious decision on the part of the developers because for an enum to take on an out-of-bounds value is usually indictive of a bug elsewhere. The crash unfortunately occurs because the compiler, in this case, creates a linear jump table and ends up trying to branch to an invalid address.

If you think the input variable has a chance of being out of bounds (because it's being directly read from a file, for example), then you need to do the range check manually.

I really hate to say something so stereotypical here... "It's not a bug, it's a feature".

Ville Krumlinde

2019-05-20 14:48

reporter   ~0116284

Last edited: 2019-05-20 14:54

View 2 revisions

I understand what you are saying but:

1. I've been involved in many projects that contain code that has this behavior (relying on case-statement not crashing on invalid enum value) and this is the first time I've seen it crash. And recently it did not crash in Fpc either so something must have changed. And this change will break existing code. I've coded in Pascal since 1985, professionally every work day since 1990. At the very least it could keep the old behavior for Delphi-mode.

2. Setting a enum to a special magic value like $FF can be an indicator that this value is "unused", and is useful for size-sensitive packed records where adding another flag would waste memory.

3. Find crash bugs like this is difficult because the jump to address outside current function body messes up the callstack in debugger.

4. Both GCC and Clang C++ compilers test that enum value is not invalid before jumping into jump table, check the generated code here: https://godbolt.org/z/Rq00k2
In my opinion it should not be possible to crash a case-statement based on a invalid value.

Please consider fixing this.

Martok

2019-05-20 15:47

reporter   ~0116286

@Kit:
> If you think the input variable has a chance of being out of bounds (because it's being directly read from a file, for example), then you need to do the range check manually.
It's actually fairly hard to do this, since the compiler may remove the check at any time - after all, the condition tested for "can't" happen. Ondrej proposed a solution some time ago, that was also rejected.

@VK:
FPC was always the *only* compiler doing this. It only crashes a lot more user code since the weight for jumptables was reduced to fairly short types. I reported the same in 2017 as 0032079. Feel free to apply the patch posted there, which is well tested, safe, and (together with 0033093 v3) costs no cycles.
And yes, this broke about 30 years worth of code for me (or actually, it didn't, because we ended up staying with Delphi for that very reason).

J. Gareth Moreton

2019-05-20 22:10

developer   ~0116293

The nuances of case blocks have been a point of contention lately. I unfortunately can't promise anything will change, but I'll see what can be brought up, especially as the behaviour of FPC seems to stand in stark contrast to other dialects of Pascal when it comes to this.

And people rejecting the use of a tool because of a language characteristic is a worrying prospect that one should at least pay attention to.

Do-wan Kim

2019-05-21 02:51

reporter  

35603_case_max_check.patch (1,323 bytes)   
Index: compiler/x86/nx86set.pas
===================================================================
--- compiler/x86/nx86set.pas	(revision 42111)
+++ compiler/x86/nx86set.pas	(working copy)
@@ -157,6 +157,7 @@
         current_asmdata.getglobaldatalabel(table);
         { make it a 32bit register }
         indexreg:=cg.makeregsize(current_asmdata.CurrAsmList,hregister,OS_INT);
+        cg.a_cmp_const_reg_label(current_asmdata.CurrAsmList,opcgsize,OC_A,aint(max_),hregister,elselabel);
         cg.a_load_reg_reg(current_asmdata.CurrAsmList,opcgsize,OS_INT,hregister,indexreg);
         { create reference }
         reference_reset_symbol(href,table,0,sizeof(pint),[]);
Index: compiler/x86_64/nx64set.pas
===================================================================
--- compiler/x86_64/nx64set.pas	(revision 42111)
+++ compiler/x86_64/nx64set.pas	(working copy)
@@ -156,6 +156,7 @@
         { local label in order to avoid using GOT }
         current_asmdata.getlabel(tablelabel,alt_data);
         indexreg:=cg.makeregsize(jtlist,hregister,OS_ADDR);
+        cg.a_cmp_const_reg_label(jtlist,opcgsize,OC_A,aint(max_),hregister,elselabel);
         cg.a_load_reg_reg(jtlist,opcgsize,OS_ADDR,hregister,indexreg);
         { load table address }
         reference_reset_symbol(href,tablelabel,0,4,[]);
35603_case_max_check.patch (1,323 bytes)   

Do-wan Kim

2019-05-21 03:53

reporter   ~0116298

min, max check on case patch. it looks good but I don't know about side effect.
35603_case_min_max_check.patch (1,520 bytes)   
Index: compiler/x86/nx86set.pas
===================================================================
--- compiler/x86/nx86set.pas	(revision 42111)
+++ compiler/x86/nx86set.pas	(working copy)
@@ -157,6 +157,8 @@
         current_asmdata.getglobaldatalabel(table);
         { make it a 32bit register }
         indexreg:=cg.makeregsize(current_asmdata.CurrAsmList,hregister,OS_INT);
+        cg.a_cmp_const_reg_label(current_asmdata.CurrAsmList,opcgsize,OC_B,aint(min_),hregister,elselabel);
+        cg.a_cmp_const_reg_label(current_asmdata.CurrAsmList,opcgsize,OC_A,aint(max_),hregister,elselabel);
         cg.a_load_reg_reg(current_asmdata.CurrAsmList,opcgsize,OS_INT,hregister,indexreg);
         { create reference }
         reference_reset_symbol(href,table,0,sizeof(pint),[]);
Index: compiler/x86_64/nx64set.pas
===================================================================
--- compiler/x86_64/nx64set.pas	(revision 42111)
+++ compiler/x86_64/nx64set.pas	(working copy)
@@ -156,6 +156,8 @@
         { local label in order to avoid using GOT }
         current_asmdata.getlabel(tablelabel,alt_data);
         indexreg:=cg.makeregsize(jtlist,hregister,OS_ADDR);
+        cg.a_cmp_const_reg_label(jtlist,opcgsize,OC_B,aint(min_),hregister,elselabel);
+        cg.a_cmp_const_reg_label(jtlist,opcgsize,OC_A,aint(max_),hregister,elselabel);
         cg.a_load_reg_reg(jtlist,opcgsize,OS_ADDR,hregister,indexreg);
         { load table address }
         reference_reset_symbol(href,tablelabel,0,4,[]);
35603_case_min_max_check.patch (1,520 bytes)   

Ville Krumlinde

2019-05-21 10:37

reporter   ~0116300

Last edited: 2019-05-21 10:44

View 3 revisions

@Martok: Thanks for info and patch. I can see you fought for this issue before and I understand your frustration.

@Do-wan-kim: thanks for patch.

@J. Gareth Moreton: Yes any issue (no matter how minor) that stops code working on the first attempt when people try Fpc will be an obstacle for new users. Additionally this issue only happens when a jump-table is used (otherwise the code works) so the code will break depending on Fpcs internal logic when to use jump-table. Which means code will crash in such situation as
1) user adds additional case-labels
2) user increase Fpc optimization level
3) user upgrades Fpc.
And since reason of crash is difficult to find it is likely the user will continue using older solution.

As for when this started, Fpc trunk revision 38654 did not have this behavior. Same code compiles to this:
# [20] case global1 of
    subl $3,%eax
    cmpl $34,%eax
    ja Lj8
    andl $4294967295,%eax
    leaq Ld1(%rip),%rdx
    movslq (%rdx,%rax,4),%rax
    addq %rdx,%rax
    jmp *%rax

Lj8 is the "else" so any out of range value will go to else-label.

nanobit

2019-05-21 10:50

reporter   ~0116301

I agree, this should be fixed. There is no need for an arbitrary crash,
internally in the selector. It's easy to say, implement own domain checks
(because not implicit in "case"), but who can control all third party sources.
The docs essentially say: Else-branch means "outside" by negation-of-inside.
This can be seen as binary test, alike: if isNumber( NAN) then ... else ...
The programmer can examine the outlier (NAN) inside the else-branch,
if filtering-out was not the only intent.

Denis Golovan

2019-05-21 10:55

reporter   ~0116302

Last edited: 2019-05-21 13:42

View 2 revisions

It's a bit scary.
Please fix this, as whole lot of old 3rd party Delphi components rely on "else" triggering in this case.

Florian

2019-05-22 20:47

administrator   ~0116346

The issue was discussed already in the duplicate report.

About the patch: please note that it does not change all targets supported by FPC.

Issue History

Date Modified Username Field Change
2019-05-20 10:17 Ville Krumlinde New Issue
2019-05-20 10:17 Ville Krumlinde File Added: project1.lpr
2019-05-20 10:17 Ville Krumlinde File Added: project1.s
2019-05-20 13:24 Marģers Note Added: 0116281
2019-05-20 13:25 J. Gareth Moreton Note Added: 0116282
2019-05-20 14:48 Ville Krumlinde Note Added: 0116284
2019-05-20 14:54 Ville Krumlinde Note Edited: 0116284 View Revisions
2019-05-20 15:47 Martok Note Added: 0116286
2019-05-20 22:10 J. Gareth Moreton Note Added: 0116293
2019-05-21 02:51 Do-wan Kim File Added: 35603_case_max_check.patch
2019-05-21 03:53 Do-wan Kim File Added: 35603_case_min_max_check.patch
2019-05-21 03:53 Do-wan Kim Note Added: 0116298
2019-05-21 10:37 Ville Krumlinde Note Added: 0116300
2019-05-21 10:44 Ville Krumlinde Note Edited: 0116300 View Revisions
2019-05-21 10:44 Ville Krumlinde Note Edited: 0116300 View Revisions
2019-05-21 10:50 nanobit Note Added: 0116301
2019-05-21 10:55 Denis Golovan Note Added: 0116302
2019-05-21 13:42 J. Gareth Moreton Note Edited: 0116302 View Revisions
2019-05-22 20:46 Florian Relationship added duplicate of 0032079
2019-05-22 20:47 Florian Status new => resolved
2019-05-22 20:47 Florian Resolution open => duplicate
2019-05-22 20:47 Florian FPCTarget => -
2019-05-22 20:47 Florian Note Added: 0116346
2019-12-10 21:10 Florian Assigned To => Florian