Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Socket limit for Select on Linux (memory corruption) #445

Open
MaxRusov opened this issue Jan 27, 2023 · 14 comments
Open

Socket limit for Select on Linux (memory corruption) #445

MaxRusov opened this issue Jan 27, 2023 · 14 comments
Assignees
Labels
Element: Socket Stacks Issues related to OS socket APIs, TIdStack and TIdSocketList descedants, etc Status: Pending Issue is pending external update or release Type: Bug Issue is a bug in existing code

Comments

@MaxRusov
Copy link

MaxRusov commented Jan 27, 2023

TIdSocketListVCLPosix.Add checks Count < FD_SETSIZE. It is incorrect in Posix. Need to check: AHandle < FD_SETSIZE.

_FD_SET causes memory corruption if AHandle > 1024.

It is advisable to replace select to poll in Linux

@rlebeau rlebeau changed the title Soket limit for Select on Linux (memory corruption) Socket limit for Select on Linux (memory corruption) Jan 27, 2023
@rlebeau rlebeau self-assigned this Jan 27, 2023
@rlebeau rlebeau added Type: Bug Issue is a bug in existing code Element: Socket Stacks Issues related to OS socket APIs, TIdStack and TIdSocketList descedants, etc Socket Stack descendants labels Jan 27, 2023
rlebeau added a commit that referenced this issue Jan 27, 2023
… the socket descriptor to FD_SETSIZE instead of comparing the list count to FD_SETSIZE.
@rlebeau
Copy link
Member

rlebeau commented Feb 1, 2023

Is this working now?

@MaxRusov
Copy link
Author

MaxRusov commented Feb 1, 2023

This is not enough for my purposes (WEB Server). I have a lot more than 1024 connections.

In my version, I use pool, it seems to work. But I'm handling a special case - only one socket at a time (Count = 1)

@rlebeau
Copy link
Member

rlebeau commented Feb 1, 2023

This is not enough for my purposes (WEB Server). I have a lot more than 1024 connections.

Per machine? Then how are you not running into file descriptor limitations outside of select()?

In any case, internally Indy uses only 1-2 sockets per TIdSocketList... instance. But I get your point. Indy will have to be updated in a future version to use poll() instead.

@dbcto
Copy link

dbcto commented Feb 2, 2023

Does the problem also exist in the Indy version that comes with Delphi 11? We also use Indy as a web server under Linux.

@rlebeau
Copy link
Member

rlebeau commented Feb 6, 2023

Does the problem also exist in the Indy version that comes with Delphi 11?

Yes, it does. And Embarcadero is already beta testing the next RAD Studio release, so a fix for this issue will likely not make it into that release, either. Maybe the release after that...

@MaxRusov
Copy link
Author

MaxRusov commented Feb 6, 2023

Per machine? Then how are you not running into file descriptor limitations outside of select()?

Per process. Limit for file descriptors can be increased by 'ulimit'

Note. Each outgoing connection also occupies one handle. If process made 1024 outgoing connection - first incoming connection got Handle=1025 and can't be selected. OOPS. My server made some outgoing connection for each incoming, so limit much less then 1024 'users'

@dbcto
Copy link

dbcto commented Feb 6, 2023

Does the problem also exist in the Indy version that comes with Delphi 11?

Yes, it does. And Embarcadero is already beta testing the next RAD Studio release, so a fix for this issue will likely not make it into that release, either. Maybe the release after that...

That's good news. However, a new release of Embarcadero may take quite a while before it is released.
Is there a relatively simple way or workaround to patch this in the current version?

@rlebeau
Copy link
Member

rlebeau commented Feb 12, 2023

Sorry for the delay in responding.

Is there a relatively simple way or workaround to patch this in the current version?

Actually, yes, I believe so. You should be able to derive a new class from TIdSocketList (nothing in TIdSocketListVCLPosix is worth inheriting) and override all of its virtual methods to operate with poll(), for example (feel free to adjust this code as needed):

type
  TMySocketList = class(TIdSocketList)
  protected
    FSockets: TList<TIdStackSocketHandle>;
    //
    class function FDPoll(ASet: TList<pollfd>; const ATimeout: Integer): Boolean;
    function GetItem(AIndex: Integer): TIdStackSocketHandle; override;
  public
    constructor Create; override;
    destructor Destroy; override;
    procedure Add(AHandle: TIdStackSocketHandle); override;
    procedure Remove(AHandle: TIdStackSocketHandle); override;
    function Count: Integer; override;
    procedure Clear; override;
    function Clone: TIdSocketList; override;
    function ContainsSocket(AHandle: TIdStackSocketHandle): Boolean; override;
    procedure GetPollList(VSet: TList<pollfd>; const AEvents: Int16);
    procedure SetPollList(VSet: TList<pollfd>; const AEvents: Int16);
    class function Select(AReadList: TIdSocketList; AWriteList: TIdSocketList;
      AExceptList: TIdSocketList; const ATimeout: Integer = IdTimeoutInfinite): Boolean; override;
    function SelectRead(const ATimeout: Integer = IdTimeoutInfinite): Boolean; override;
    function SelectReadList(var VSocketList: TIdSocketList;
      const ATimeout: Integer = IdTimeoutInfinite): Boolean; override;
  end;

constructor TMySocketList.Create;
begin
  inherited;
  FSockets := TList<TIdStackSocketHandle>.Create;
end;

destructor TMySocketList.Destroy;
begin
  FSockets.Free;
  inherited;
end;

procedure TMySocketList.Add(AHandle: TIdStackSocketHandle);
begin
  Lock;
  try
    if not FSockets.Contains(AHandle) then begin
      FSockets.Add(AHandle);
    end;
  finally
    Unlock;
  end;
end;

procedure TMySocketList.Clear;
begin
  Lock;
  try
    FSockets.Clear;
  finally
    Unlock;
  end;
end;

function TMySocketList.Clone: TIdSocketList;
begin
  Result := TMySocketList.Create;
  try
    Lock;
    try
      TMySocketList(Result).FSockets.Assign(FSockets);
    finally
      Unlock;
    end;
  except
    FreeAndNil(Result);
    raise;
  end;
end;

function TMySocketList.ContainsSocket(
  AHandle: TIdStackSocketHandle): Boolean;
begin
  Lock;
  try
    Result := FSockets.Contains(AHandle);
  finally
    Unlock;
  end;
end;

function TMySocketList.Count: Integer;
begin
  Lock;
  try
    Result := FSockets.Count;
  finally
    Unlock;
  end;
end;

class function TMySocketList.FDPoll(ASet: TList<pollfd>;
  const ATimeout: Integer): Boolean;
begin
  Result := poll(ppoolfd(ASet.List), ASet.Count, ATimeout) > 0;
end;

procedure TMySocketList.GetPollList(var VSet: TList<pollfd>;
  const AEvents: Int16);
var
  LPollFD: pollfd;
  I: Integer;
begin
  Lock;
  try
    for I := 0 to FSockets.Count-1 do begin
      LPollFD.fd := FSockets[i];
      LPollFD.events := AEvents;
      LPollFD.revents := 0;
      VSet.Add(LPollFD);
    end;
  finally
    Unlock;
  end;
end;

function TMySocketList.GetItem(AIndex: Integer): TIdStackSocketHandle;
begin
  Lock;
  try
    if (AIndex >= 0) and (AIndex < FSockets.Count) then begin
      Result := FSockets[AIndex];
    end else begin
      Result := -1;
    end;
  finally
    Unlock;
  end;
end;

procedure TMySocketList.Remove(AHandle: TIdStackSocketHandle);
begin
  Lock;
  try
    FSockets.Remove(AHandle);
  finally
    Unlock;
  end;
end;

class function TMySocketList.Select(AReadList, AWriteList,
  AExceptList: TIdSocketList; const ATimeout: Integer): Boolean;
var
  LPollList: TList<pollfd>;

  procedure SetCount(AList: TIdSocketList);
  begin
    if AList <> nil then begin
      Result := AList.Count;
    end else begin
      Result := 0;
    end;
  end;

  procedure ReadSet(AList: TIdSocketList; const AEvents: Shortint);
  begin
    if AList <> nil then begin
      TMySocketList(AList).GetPollList(LPollList, AEvents);
    end;
  end;

begin
  LPollList := TList<pollfd>.Create;
  try
    LPollList.Capacity := SetCount(AReadList) + SetCount(AWriteList) + SetCount(AExceptList);
    ReadSet(AReadList, POLLIN or POLLRDHUP);
    ReadSet(AWriteList, POLLOUT);
    ReadSet(AExceptList, POLLPRI);
    //
    Result := FDPoll(LPollList, ATimeout);
    //
    if AReadList <> nil then begin
      TMySocketList(AReadList).SetPollList(LPollList, POLLIN or POLLHUP);
    end;
    if AWriteList <> nil then begin
      TMySocketList(AWriteList).SetPollList(LPollList, POLLOUT);
    end;
    if AExceptList <> nil then begin
      TMySocketList(AExceptList).SetPollList(LPollList, POLLPRI);
    end;
  finally
    LPollList.Free;
  end;
end;

function TMySocketList.SelectRead(const ATimeout: Integer): Boolean;
var
  LPollList: TList<pollfd>;
begin
  LPollList := TList<pollfd>.Create;
  try
    Lock;
    try
      LPollList.Capacity := FSockets.Count;
      GetPollList(LPollList, POLLIN or POLLRDHUP);
    finally
      Unlock;
    end;
    Result := FDPoll(LPollList, ATimeout);
  finally
    LPollList.Free;
  end;
end;

function TMySocketList.SelectReadList(var VSocketList: TIdSocketList;
  const ATimeout: Integer): Boolean;
var
  LPollList: TList<pollfd>;
begin
  LPollList := TList<pollfd>.Create;
  try
    Lock;
    try
      LPollList.Capacity := FSockets.Count;
      GetPollList(LPollList, POLLIN or POLLRDHUP);
    finally
      Unlock;
    end;
    Result := FDPoll(LPollList, ATimeout);
    if Result then begin
      if VSocketList = nil then begin
        VSocketList := TIdSocketList.CreateSocketList;
      end;
      TMySocketList(VSocketList).SetPollList(LPollList, 0);
    end;
  finally
    LPollList.Free;
  end;
end;

procedure TMySocketList.SetPollList(VSet: TList<pollfd>;
  const AEvents: Int16;
var
  LPollFD: pollfd;
  I: Integer;
begin
  Lock;
  try
    FSockets.Clear;
    for I := 0 to VSet.Count-1 do begin
      LPollFD := VSet[I];
      if (AEvents = 0) or ((LPollFD.revents and AEvents) <> 0) then begin
        FSockets.Add(LPollFD.fd);
      end;
    end;
  finally
    Unlock;
  end;
end;

And then, at runtime you can assign your new class type to Indy's global GSocketListClass variable in the IdStack unit:

IdStack.GSocketListClass := TMySocketList;

The default platform-specific TIdSocketListXXX classes are all dependent on their platform's fd_set type, and they are unfortunately declared in the interface section of their respective IdStackXXX units, so applying this fix in the main code will require interface-breaking changes. Since all of the classes logic are private, the classes should have all been declared in their respective unit's implementation section rather than the interface section, then this would not have been an issue to patch. I'll have to fix that in a future version.

@dbcto
Copy link

dbcto commented Feb 13, 2023

Thank you very much! We will try that and let you know if it worked.

@msccto
Copy link

msccto commented Mar 8, 2023

@rlebeau I tried to implement the workaround you suggest using your code above. However I run into several problems and can not compile it.
For example, FSockets is of type System.Generics.Collections.TList<>:
FSockets: TList<TIdStackSocketHandle>;
but there is no Assign method available causing a compile error in
TMySocketList(Result).FSockets.Assign(FSockets);

Another problem is that I don't have poll function, causing an error in
Result := poll(ppoolfd(ASet.List), ASet.Count, ATimeout) > 0;

Am I missing something? Appreciate any help.

@rlebeau
Copy link
Member

rlebeau commented Mar 8, 2023

However I run into several problems and can not compile it.

I wrote that example from scratch off the top of my head, I didn't actually test it. I did say to "feel free to adjust this code as needed", which would include tweaking it to make it compile :-)

there is no Assign method available

The non-generic TList has an Assign() method, so I assumed the generic TList did as well. Apparently not. In which case, use the Clear() and AddRange() methods instead, eg:

with TMySocketList(Result).FSockets do
begin
  Clear;
  AddRange(Self.FSockets);
end;

Another problem is that I don't have poll function

Obviously, you will need to add the appropriate declaring unit in your uses clause (offhand, I don't know which unit that is), or else declare the function in your own code.

@rlebeau
Copy link
Member

rlebeau commented Apr 23, 2023

How is this looking? Were you able to get it working?

@rlebeau rlebeau added the Status: Pending Issue is pending external update or release label Apr 23, 2023
@msccto
Copy link

msccto commented Apr 24, 2023

We were looking for another solution so far, however we try to get it working using your approach in the next days. We'll keep you posted.

@msccto
Copy link

msccto commented Apr 26, 2023

I finally was able to get it working using your code and it works good so far.
My test application using TIdHTTPServer with original implementation crashed on Linux when there were more than 1024 clients trying to connect - with the new implementation it worked very good with more than 7000 clients.

I also created a PR #473

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Element: Socket Stacks Issues related to OS socket APIs, TIdStack and TIdSocketList descedants, etc Status: Pending Issue is pending external update or release Type: Bug Issue is a bug in existing code
Projects
None yet
Development

No branches or pull requests

4 participants