// Linux TCP and verified TLS clients; isolated from interpreter dispatch. static bool foundationOperation(const std::string&); static void foundationValidate(const CodeLine&); static void socketValidate(const CodeLine& c) { count(c,2,7); const auto& sub=c.arg[1].text; if(foundationOperation(sub)) { foundationValidate(c); return; } if(sub=="connect" || sub=="tlsconnect" || sub=="listen") { count(c,6,6); destination(c.arg[2],c.source); } else if(sub=="accept") { count(c,5,5); destination(c.arg[2],c.source); } else if(sub=="sendall") count(c,5,5); else if(sub=="recv" || sub=="readline") { count(c,7,7); destination(c.arg[2],c.source); destination(c.arg[3],c.source); } else if(sub=="endpoint") { count(c,5,5); destination(c.arg[2],c.source); destination(c.arg[3],c.source); } else if(sub=="close") count(c,3,3); else fail(c.source,"unknown socket operation"); } using SocketClock=std::chrono::steady_clock; static void socketDeadline(SocketClock::time_point deadline,const Source& s) { if(SocketClock::now()>=deadline) fail(s,"socket timeout"); } static void socketWait(int fd,short events,SocketClock::time_point deadline,const Source& s) { for(;;) { socketDeadline(deadline,s); auto ns=std::chrono::duration_cast(deadline-SocketClock::now()).count(); int ms=static_cast(std::max(1,(ns+999999)/1000000)); pollfd p{fd,events,0}; int rc=::poll(&p,1,ms); if(rc<0 && errno==EINTR) continue; if(rc<0) { if(errno==ENOMEM || errno==ENOBUFS) failLimit(s,"socket poll resource limit"); fail(s,"socket poll failed: "+std::string(std::strerror(errno))); } if(rc>0) { if(p.revents&POLLNVAL) fail(s,"socket invalid descriptor"); return; } } } #include "socket_dns.inc" #include "socket_tls.inc" #include "websocket_protocol.inc" struct vScript::Socket { int fd=-1; bool listener=false, eof=false; bool datagram=false, handshaking=false; short want=0, readWant=0; std::string pendingWrite, pendingRead; std::size_t readMax=0; SocketClock::time_point handshakeDeadline{}; std::string buffer; std::unique_ptr tls; std::unique_ptr ws; bool wsBuffered() const { return ws && ws->parseReady && pendingWrite.empty() && ws->queue.empty() && !(tls && !pendingRead.empty() && readWant==POLLOUT); } ~Socket() { tls.reset(); if(fd>=0) ::close(fd); } }; #include "socket_foundations.inc" #include "websocket.inc" bool vScript::socketCommand(Frame& frame,const CodeLine& c) { const auto& a=c.arg; const auto& s=c.source; const auto& sub=a[1].text; if(foundationOperation(sub)) return foundationCommand(frame,c); auto socketOS=[&](const Source& source,const std::string& operation) { int error=errno; if(error==ENOMEM || error==ENOBUFS || error==EMFILE || error==ENFILE) { // Fatal: release owned descriptors before diagnostic machinery runs. // In particular sanitizer diagnostics themselves need spare FDs. sockets.clear(); failLimit(source,"socket OS resource limit: "+operation); } fail(source,"socket "+operation+": "+std::strerror(error)); }; auto val=[&](std::size_t i) { return value(frame,a[i],s); }; auto put=[&](std::size_t i,const std::string& x) { assign(frame,a[i],x,s); }; auto dest=[&](std::size_t i) { auto& v=lookup(frame,a[i],s); if(v.array || v.structured) fail(s,"socket destination must be scalar"); }; auto outputs=[&](std::size_t max) { dest(2); if(sub=="recv" || sub=="readline" || sub=="endpoint") { dest(3); if(a[2].text==a[3].text) fail(s,"socket destinations must differ"); } // Conservative reservation BEFORE effects, including all retained capacities. std::size_t total=frame.extraBytes+max+256; for(const auto& entry:frame.vars) total+=variableSize(entry.second)+entry.first.capacity(); if(total>frameLimit) failLimit(s,"socket frame reservation limit"); }; if(sub=="connect" || sub=="tlsconnect" || sub=="listen" || sub=="accept" || sub=="endpoint") outputs(128); if(!options.allowNetwork) fail(s,"socket operations require --allow-network (not a sandbox)"); if(sub=="listen" && !options.allowListen) fail(s,"socket listen requires --allow-listen and --allow-network"); if(++socketCalls>4096) failLimit(s,"socket call limit (4096)"); auto handle=[&](std::size_t i) { auto key=val(i); auto it=sockets.find(key); if(it==sockets.end()) fail(s,"invalid or closed socket handle"); if(it->second->datagram) fail(s,"socket operation requires TCP handle"); if(it->second->ws && sub!="close" && sub!="endpoint") fail(s,"raw socket I/O forbidden on websocket"); return it; }; auto timeout=[&](std::size_t i) { auto ms=static_cast(integer(val(i),1,30000,s)); if(ms>60000-socketTime) failLimit(s,"socket cumulative timeout reservation limit (60000 ms)"); socketTime+=ms; return SocketClock::now()+std::chrono::milliseconds(ms); }; auto room=[&]() { if(sockets.size()>=32) failLimit(s,"socket handle limit (32)"); }; auto store=[&](const std::shared_ptr& p) { auto id="tcp:"+std::to_string(nextSocket++); sockets.emplace(id,p); put(2,id); }; if(sub=="connect" || sub=="tlsconnect" || sub=="listen") { auto originalHost=val(3), host=originalHost; bool listener=sub=="listen"; if(host=="localhost" || (host.empty() && listener)) host="127.0.0.1"; auto port=static_cast(integer(val(4),listener ? 0 : 1,65535,s)); socketHost(host,listener,s); int backlog=0; SocketClock::time_point deadline; if(listener) backlog=static_cast(integer(val(5),1,128,s)); else deadline=timeout(5); room(); std::vector addresses; if(listener) addresses.push_back(socketNumeric(host,port)); else { if(!socketNumeric(host,port).length && ++dnsCalls>128) failLimit(s,"socket DNS lookup limit (128)"); try { addresses=socketResolve(host,port,options,deadline,s,[&]() { sockets.clear(); }); } catch(const Error& e) { if(e.fatal) sockets.clear(); throw; } } std::shared_ptr p; int lastError=ECONNREFUSED; for(const auto& address:addresses) { if(!listener) socketDeadline(deadline,s); p=std::make_shared(); p->listener=listener; int family=address.address.ss_family; p->fd=::socket(family,SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC,0); if(p->fd<0) socketOS(s,"create"); if(listener) { if(family==AF_INET6) { int yes=1; if(setsockopt(p->fd,IPPROTO_IPV6,IPV6_V6ONLY,&yes,sizeof(yes))<0) socketOS(s,"IPv6-only"); } if(::bind(p->fd,reinterpret_cast(&address.address),address.length)<0) socketOS(s,"bind"); if(::listen(p->fd,backlog)<0) socketOS(s,"listen"); break; } int rc=::connect(p->fd,reinterpret_cast(&address.address),address.length); int error=rc==0 ? 0 : errno; if(rc<0 && (error==EINPROGRESS || error==EINTR || error==EALREADY)) { socketWait(p->fd,POLLOUT,deadline,s); socklen_t len=sizeof(error); if(getsockopt(p->fd,SOL_SOCKET,SO_ERROR,&error,&len)<0) socketOS(s,"connect status"); } if(!error) break; if(error==ENOMEM || error==ENOBUFS || error==EMFILE || error==ENFILE) { errno=error; socketOS(s,"connect"); } lastError=error; p.reset(); } if(!p) { errno=lastError; socketOS(s,"connect"); } if(!listener) socketDeadline(deadline,s); if(sub=="tlsconnect") { p->tls=std::make_unique(); p->tls->releaseResources=[this,owned=p.get()]() { if(owned->fd>=0) { ::close(owned->fd); owned->fd=-1; } sockets.clear(); }; try { p->tls->connect(p->fd,originalHost,options,deadline,s); } catch(const Error& e) { if(e.fatal) sockets.clear(); throw; } } store(p); return true; } if(sub=="close") { auto it=handle(2); if(it->second->tls && !it->second->handshaking && it->second->pendingWrite.empty() && it->second->pendingRead.empty()) it->second->tls->close(); sockets.erase(it); return true; } if(sub=="endpoint") { auto p=handle(4)->second; sockaddr_storage addr{}; socklen_t len=sizeof(addr); if(getsockname(p->fd,reinterpret_cast(&addr),&len)<0) socketOS(s,"endpoint"); char text[INET6_ADDRSTRLEN]; unsigned port; if(addr.ss_family==AF_INET) { auto* x=reinterpret_cast(&addr); inet_ntop(AF_INET,&x->sin_addr,text,sizeof(text)); port=ntohs(x->sin_port); } else { auto* x=reinterpret_cast(&addr); inet_ntop(AF_INET6,&x->sin6_addr,text,sizeof(text)); port=ntohs(x->sin6_port); } put(2,text); put(3,std::to_string(port)); return true; } if(sub=="accept") { auto p=handle(3)->second; if(!p->listener) fail(s,"socket accept requires listener"); if(p->tls) fail(s,"TLS listener requires tryaccept and handshake"); auto deadline=timeout(4); room(); auto client=std::make_shared(); for(;;) { socketDeadline(deadline,s); client->fd=::accept4(p->fd,nullptr,nullptr,SOCK_NONBLOCK|SOCK_CLOEXEC); if(client->fd>=0) break; if(errno==EINTR) continue; if(errno==EAGAIN || errno==EWOULDBLOCK) { socketWait(p->fd,POLLIN,deadline,s); continue; } socketOS(s,"accept"); } store(client); return true; } bool reading=sub=="recv" || sub=="readline"; auto connection=handle(reading ? 4 : 2); auto p=connection->second; auto connectionKey=connection->first; if(p->listener) fail(s,"socket operation requires connection, not listener"); if(p->handshaking) { if(SocketClock::now()>=p->handshakeDeadline) { sockets.erase(connectionKey); fail(s,"socket TLS handshake timeout"); } fail(s,"socket TLS handshake incomplete"); } if(!p->pendingWrite.empty() || !p->pendingRead.empty()) fail(s,"socket pending operation conflicts with legacy I/O"); if(!reading) { auto data=val(3); auto deadline=timeout(4); if(data.size()>16*valueLimit-socketBytes) failLimit(s,"socket cumulative byte limit (16 MiB)"); // Charge attempted writes; retries cannot evade the total bound. socketBytes+=data.size(); std::size_t sent=0; try { while(senttls ? p->tls->perform(2,data.data()+sent,static_cast(data.size()-sent),deadline,s) : ::send(p->fd,data.data()+sent,data.size()-sent,MSG_NOSIGNAL); if(n>0) { sent+=static_cast(n); if(p->tls) socketDeadline(deadline,s); continue; } if(n<0 && errno==EINTR) continue; if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK)) { socketWait(p->fd,POLLOUT,deadline,s); continue; } socketOS(s,"send"); } } catch(const Error& e) { if(p->tls) sockets.erase(connectionKey); if(e.fatal) sockets.clear(); throw Error(s,std::string(e.what())+"; sent="+std::to_string(sent)+" of "+std::to_string(data.size())+" bytes (do not blindly retry)",e.fatal); } return true; } auto max=static_cast(integer(val(5),1,65536,s)); outputs(max); auto deadline=timeout(6); for(;;) { auto newline=p->buffer.find('\n'); bool line=sub=="readline"; if((!line && !p->buffer.empty()) || (line && newline!=std::string::npos && newlineeof) { auto n=std::min(max,p->buffer.size()); bool terminated=line && newlinebuffer.substr(0,terminated ? n-1 : n)); put(3,p->eof && n==p->buffer.size() ? "1" : "0"); p->buffer=std::string(p->buffer.data()+n,p->buffer.size()-n); return true; } if(line && p->buffer.size()>=max) fail(s,"socket line limit (buffer preserved; drain with recv)"); std::size_t want=std::min(8192,max-p->buffer.size()); if(want>16*valueLimit-socketBytes) failLimit(s,"socket cumulative byte limit (16 MiB)"); // Rebuild each append to bound retained capacity, not just logical length. std::size_t retained=0; for(const auto& entry:sockets) retained+=entry.second->buffer.capacity()+(entry.second->pendingRead.empty() ? 0 : entry.second->pendingRead.capacity())+(entry.second->pendingWrite.empty() ? 0 : entry.second->pendingWrite.capacity())+(entry.second->ws ? entry.second->ws->storage() : 0); if(retained+2*(p->buffer.size()+want)>valueLimit) failLimit(s,"socket buffer storage limit (1 MiB)"); char bytes[8192]; ssize_t n; if(p->tls) { try { n=p->tls->perform(1,bytes,static_cast(want),deadline,s); } catch(const Error& e) { sockets.erase(connectionKey); if(e.fatal) sockets.clear(); throw; } } else { socketDeadline(deadline,s); n=::recv(p->fd,bytes,want,0); } if(n>0) { socketBytes+=static_cast(n); std::string fresh=p->buffer; fresh.append(bytes,static_cast(n)); p->buffer.swap(fresh); continue; } if(n==0) { p->eof=true; continue; } if(errno==EINTR) continue; if(errno==EAGAIN || errno==EWOULDBLOCK) { socketWait(p->fd,POLLIN,deadline,s); continue; } socketOS(s,"recv"); } }