Recent

Author Topic: Impure methods in advanced records, constness & undefined behavior  (Read 1141 times)

Khrys

  • Sr. Member
  • ****
  • Posts: 390
The following snippet (rightly) fails to compile:

Code: Pascal  [Select][+][-]
  1. procedure Foo(const Items: array of Integer);
  2. begin
  3.   SetLength(Items, 100); // Error: Can't assign values to const variable
  4. end;

This snippet, however, compiles just fine - even though it shouldn't:

Code: Pascal  [Select][+][-]
  1. type
  2.   TContainer = record
  3.     Items: array of Integer;
  4.     procedure Resize(Count: SizeInt);
  5.   end;
  6.  
  7. procedure TContainer.Resize(Count: SizeInt);
  8. begin
  9.   SetLength(Items, Count);
  10. end;
  11.  
  12. procedure Foo(const Container: TContainer);
  13. begin
  14.   Container.Resize(100); // Completely fine (unless `Resize` is inlined)
  15. end;

The issue seems to be that constness can't be expressed for the implicit  Self  parameter in methods, so the compiler defaults to allowing full access.

I noticed this while using Address Sanitizer on an advanced record with this exact structure (wrapping a dynamic array). It reported a use-after-free in the code following the call to  Foo,  and to my shock it was completely right - the backing allocation had been changed by illegally calling  SetLength  on the array, and the instance in the caller's scope (which should have been unchanged) was now pointing to freed memory.

You can take a look at this yourself by compiling the following program with  heaptrc (-gh)  enabled  (to visualize the dangling pointer):

Code: Pascal  [Select][+][-]
  1. program UB_Const;
  2.  
  3. {$mode objfpc}{$H+}
  4. {$modeswitch advancedrecords}
  5.  
  6. type
  7.   generic TContainer<T> = record
  8.     Items: array of T;
  9.     procedure Resize(Count: SizeInt);
  10.   end;
  11.  
  12.   TByteContainer = specialize TContainer<Byte>;
  13.  
  14. procedure TContainer.Resize(Count: SizeInt);
  15. begin
  16.   SetLength(Items, Count);
  17. end;
  18.  
  19. procedure Foo(const Container: TByteContainer);
  20. begin
  21.   Container.Resize(100);
  22. end;
  23.  
  24. var
  25.   Data: TByteContainer;
  26.  
  27. begin
  28.  
  29.   SetLength(Data.Items, 1);
  30.   Data.Items[0] := 42;
  31.  
  32.   Foo(Data);
  33.  
  34.   WriteLn(Data.Items[0]); // BOOM - will print bytes (bites?) of $badf00d (heaptrc)
  35. end.

C++ has  const  and non-const  methods, Rust has  &self  and  &mut self,  but what about Pascal?
Can anything even be done about this without fundamentally changing to the way  const  works? I really really do not like being exposed to potential undefined behavior just by calling a method. I mean, even C provides more safety in this scenario by handling pointers to  const  objects properly.



Edit: Fixed mistake in C example
« Last Edit: October 29, 2025, 03:09:35 pm by Khrys »

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 12634
  • FPC developer.
Re: Impure methods in advanced records, constness & undefined behavior
« Reply #1 on: October 29, 2025, 03:34:23 pm »
Keep in mind that a class is a reference. The CONST part governs assignment to the reference, not the instance that the reference points too.

Khrys

  • Sr. Member
  • ****
  • Posts: 390
Re: Impure methods in advanced records, constness & undefined behavior
« Reply #2 on: October 29, 2025, 03:44:24 pm »
Keep in mind that a class is a reference. The CONST part governs assignment to the reference, not the instance that the reference points too.

I'm talking about advanced records, not classes... (you're right of course, classes don't have this problem).

Thaddy

  • Hero Member
  • *****
  • Posts: 18729
  • To Europe: simply sell USA bonds: dollar collapses
Re: Impure methods in advanced records, constness & undefined behavior
« Reply #3 on: October 29, 2025, 03:49:28 pm »
Compiled your demo in trunk:
Code: Text  [Select][+][-]
  1. 42
  2. Heap dump by heaptrc unit of "c:\Users\thadd\UB_const.exe"
  3. 1 memory blocks allocated : 133
  4. 1 memory blocks freed     : 133
  5. 0 unfreed memory blocks : 0
  6. True heap size : 65536 (128 used in System startup)
  7. True free heap : 65408
Seems fixed? No because above is windows11-64.

Linux64 gives me this:
Code: Text  [Select][+][-]
  1. 42
  2. Heap dump by heaptrc unit of "./UB_const"
  3. 1 memory blocks allocated : 133
  4. 1 memory blocks freed     : 34
  5. 0 unfreed memory blocks : 99
  6. True heap size : 65536
  7. True free heap : 65440
  8. Should be : 65437
  9. Call trace for block $00007681938FB040 size 116
  10.   $0000000000401217  Resize,  line 16 of UB_const.pas
  11.   $000000000040123D  Foo,  line 21 of UB_const.pas
  12.   $00000000004012A4  $main,  line 32 of UB_const.pas

Both are compiled with the same trunk version from 2025/10/26

Platform issue?? Should not compile....
« Last Edit: October 29, 2025, 04:03:39 pm by Thaddy »
If Europe sells their USA bonds the USD will collapse. Europe can affort that given average state debts. The USA can't affort that. Just an advice...

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12089
  • Debugger - SynEdit - and more
    • wiki
Re: Impure methods in advanced records, constness & undefined behavior
« Reply #4 on: October 29, 2025, 04:01:25 pm »
Keep in mind that a class is a reference. The CONST part governs assignment to the reference, not the instance that the reference points too.

Bit more detailed.

1) Calling the method indeed takes a pointer to the record.

The record is declared const (though for the meaning of that, see "2" below).
(self is (like) a var param in case of an advanced record)

Code: Pascal  [Select][+][-]
  1. TContainer = record
  2.     Items: array of Integer;
  3.     procedure Resize(Count: SizeInt);
  4.   end;
  5. PContainer = ^TContainer;
  6.  
  7. procedure TContainer.Resize(Count: SizeInt);
  8. begin
  9.   SetLength(Items, Count);
  10. end;
  11.  
  12. procedure Foo(const Container: TContainer);
  13. begin
  14.   //SetLength(Container.Items, 1);  // fails
  15.  
  16.   SetLength(PContainer(@Container)^.Items, 1);  // compiles / but is wrong code (see "2")
  17.   Container.Resize(100); // Completely fine (unless `Resize` is inlined)
  18. end;
  19.  

The compiler does not know that the pointer is to the const var. Well, it could in this case, but you can always make it arbitrary complex.

2) "const param"

Check the docs.

It says something along the lines "const" means the programmer promises to the compiler, that the code (written by the programmer) will not modify the value.

- "const param" are not an instruction to the compiler to check that (even though it will check the easy cases).
- "const param" are a promise to the compiler (checked by the user) to allow the compiler to perform various optimizations.

therefore, if you make that promise and break it, then your wrote invalid code. And such code may have undefined behaviour (wrong results, crashes, anything).

There are examples demonstrating this with ansistrings, if you search the forum.



It may be, that improved versions of the compiler will in the future detect this case (even if not inlining / but according to the doc, they don't have to).
« Last Edit: October 29, 2025, 04:06:45 pm by Martin_fr »

Khrys

  • Sr. Member
  • ****
  • Posts: 390
Re: Impure methods in advanced records, constness & undefined behavior
« Reply #5 on: October 29, 2025, 04:16:16 pm »
Both are compiled with the same trunk version from 2025/10/26

Platform issue?? Should not compile....

FWIW this is the output I got when running under ASAN (compiler from two months ago):



$ fpc-llvm -Clfsanitize=address ub_const.pas && ./ub_const

Free Pascal Compiler version 3.3.1 [2025/08/22] for x86_64
Copyright (c) 1993-2025 by Florian Klaempfl and others
Target OS: Linux for x86-64
Compiling ub_const.pas
Assembling ub_const
Linking ub_const
36 lines compiled, 0.2 sec, 198931 bytes code, 32661 bytes data
=================================================================
==7512==ERROR: AddressSanitizer: heap-use-after-free on address 0x604000000030 at pc 0x00000040182a bp 0x7ffdae236130 sp 0x7ffdae236128
READ of size 1 at 0x604000000030 thread T0
Runtime error 216 at $00007FD93D27B8EE
  $00007FD93D27B8EE

AddressSanitizer: nested bug in the same thread, aborting.




It says something along the lines "const" means the programmer promises to the compiler, that the code (written by the programmer) will not modify the value.

- "const param" are not an instruction to the compiler to check that (even though it will check the easy cases).
- "const param" are a promise to the compiler (checked by the user) to allow the compiler to perform various optimizations.

therefore, if you make that promise and break it, then your wrote invalid code. And such code may have undefined behaviour (wrong results, crashes, anything).

Yes, I know that  const  has little concrete meaning in FPC. I just think it's a sad state of affairs that one has to decide between code that is fast (no unnecessary copying →  const)  and code that is correct (as evidenced by this issue - without  const).

And "checked by the user" is exactly what a compiler is supposed to minimize, IMHO  O:-)

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12089
  • Debugger - SynEdit - and more
    • wiki
Re: Impure methods in advanced records, constness & undefined behavior
« Reply #6 on: October 29, 2025, 04:25:47 pm »
Yes, I know that  const  has little concrete meaning in FPC. I just think it's a sad state of affairs that one has to decide between code that is fast (no unnecessary copying →  const)  and code that is correct (as evidenced by this issue - without  const).

And "checked by the user" is exactly what a compiler is supposed to minimize, IMHO  O:-)

Well those are simply 2 features, with some overlap.

Only the one (the one where the user makes a promise) is implemented.
The other one is just missing.

And "user promises" (in a wider sense) exist in other languages too.
E.g. volatile => I don't think any compiler checks that the var indeed needs that (I would even think in most cases a compiler can not check that / except if the var is only used in one thread, and no pointer or other reach to the mem from any other thread => i.e. if there is only one single thread).



Warfley

  • Hero Member
  • *****
  • Posts: 2038
Re: Impure methods in advanced records, constness & undefined behavior
« Reply #7 on: October 29, 2025, 07:57:58 pm »
Well the concept of constness for parameters was relatively sound originally, back when pascal was procedural, with the introduction of OOP, this was not thought through well enough and this results in such oddities. It's like with variant records, they were sound in the original pascal, but with the introduction of managed types Borland didn't think it through so they are just broken with them.
Note that it's not even new with advanced records, this problem exists since objects in mode Turbo Pascal:
Code: Pascal  [Select][+][-]
  1. {$Mode TP}
  2.  
  3. type
  4.   TTest = object
  5.   public
  6.     i: Integer;
  7.     procedure SetI(AValue: Integer);
  8.   end;
  9.  
  10. procedure TTest.SetI(AValue: Integer);
  11. begin
  12.   i:=AValue;
  13. end;
  14.  
  15. procedure Foo(const t: TTest);
  16. begin        
  17.   t.SetI(42); // Works
  18.   t.i:=42; // Doesn't
  19. end;
Or operator overloading and generics:
Code: Pascal  [Select][+][-]
  1. type
  2.   ITest = interface
  3.     function Add(const rhs: ITest): ITest;
  4.   end;
  5.  
  6. operator +(const lhs, rhs: ITest): ITest;
  7. begin
  8.   Result:=lhs.Add(rhs);
  9. end;
  10.  
  11. type
  12.   // No way to define operator + for this
  13.   generic IGenericTest<T> = interface
  14.     function Add(const rhs: specialize IGenericTest<T>): specialize IGenericTest<T>;
  15.   end;

There are just some things about the language which are not well designed, because when adding new features, someone forgot about the existing features.
« Last Edit: October 29, 2025, 08:01:52 pm by Warfley »

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12089
  • Debugger - SynEdit - and more
    • wiki
Re: Impure methods in advanced records, constness & undefined behavior
« Reply #8 on: October 29, 2025, 08:06:54 pm »
Well the concept of constness for parameters was relatively sound originally, back when pascal was procedural,

Well "relatively".... Pointers have existed for a long time, haven't they?

Though, I haven't checked if the meaning of constness was the same then.

Otherwise, one of the core differences between the "const param" as we know it, and a "const something" that is (and can be) protected by the compiler, is that the "const param" also is meant to indicate, that nowhere in the entire app, does exist any pointer to the param, or if so, then that pointer is not used.

I don't think a compiler exists that could for every app prove, that no such pointer exists. (Well, unless we accept heavy amount of false positives, and consequent errors for code that does not break the rule).

And such "outside pointers" that might then trigger change while being in the function (function with const param, including nested calls to any other func) they could have existed, couldn't they?


All that OOP has done, is hiding those pointers. But then so did "var param" to normal procedure (at whatever time they were introduced).

440bx

  • Hero Member
  • *****
  • Posts: 6069
Re: Impure methods in advanced records, constness & undefined behavior
« Reply #9 on: October 29, 2025, 09:34:06 pm »
The problem is that the syntactic structure of the Pascal language does not make constness part of the data type.  Instead constness is a modifier which is much more limited than having it as part of the data type.

As a modifier it can easily be circumvented using pointers.  (oops! safety went out the window again!... <chuckle>)
FPC v3.2.2 and Lazarus v4.0rc3 on Windows 7 SP1 64bit.

Thaddy

  • Hero Member
  • *****
  • Posts: 18729
  • To Europe: simply sell USA bonds: dollar collapses
Re: Impure methods in advanced records, constness & undefined behavior
« Reply #10 on: October 30, 2025, 08:03:22 am »
If you can make the correct mind switch to interpret const vs static you most definitely can do this:
Code: Pascal  [Select][+][-]
  1. {$mode objfpc}{$modeswitch advancedrecords}
  2. type
  3.   TTest = record
  4.           i: Integer;
  5.           procedure SetValue(AValue: Integer);
  6.           end;
  7.          
  8.   procedure TTest.SetValue(AValue:integer);
  9.   begin
  10.     i:= AValue;
  11.   end;
  12.  
  13. procedure seewhathappens(const value:TTest);
  14. begin
  15.   value.setvalue(1000);
  16. end;
  17. // compiles in J+/- state, but only + state works. Otherwise runerror 216
  18. const
  19. {$push}{$J-}
  20.   test:TTest =(i:0);
  21. {$pop}
  22. begin
  23.   SeeWhatHappens(test);
  24. end.
This is not to say typed const parameters can be protected from modifying fields in the same way on its own: it has different meaning: contract to not replace the value of the record itself, but using its methods is valid at that point. If you pass the const parameter as a true const, however, the record is protected from all modification.
I can imagine some see some logic in that. %)
Anyway, pointer cheats won't help you.
« Last Edit: October 30, 2025, 11:58:41 am by Thaddy »
If Europe sells their USA bonds the USD will collapse. Europe can affort that given average state debts. The USA can't affort that. Just an advice...

PascalDragon

  • Hero Member
  • *****
  • Posts: 6311
  • Compiler Developer
Re: Impure methods in advanced records, constness & undefined behavior
« Reply #11 on: October 30, 2025, 09:30:00 pm »
C++ has  const  and non-const  methods, Rust has  &self  and  &mut self,  but what about Pascal?
Can anything even be done about this without fundamentally changing to the way  const  works? I really really do not like being exposed to potential undefined behavior just by calling a method. I mean, even C provides more safety in this scenario by handling pointers to  const  objects properly.

The compiler simply has no concept of const-methods, thus there can't be any restrictions on methods considering that the calling of methods on a const-parameter is also required for Delphi-compatiblity.

 

TinyPortal © 2005-2018