bzflag: 3 CWE-407 defects, MOAD 0002-0005 CLEAN

bzflag-0001: bz_EventHandler::HasEvent() std::find on HandledEvents vector
  called per-handler per-event-fire in callEvents hot path. O(E*H).
  Fix: std::bitset<bz_eLastEvent>. HIGH, 8.4x speedup.

bzflag-0002: AccessControlList ban/hostBan/idBan std::find on growing
  ban vector for dedup. O(B^2) during merge() of master ban list.
  Fix: parallel unordered_set index. MEDIUM, 17x speedup.

bzflag-0003: parsePermissionString customPerms std::find dedup O(W*C).
  Fix: std::set shadow for dedup. LOW-MEDIUM, 5.1x speedup.

MOAD-0004: bzfs.cxx:4732 logs auth token verbatim at debug level 1
  (logDebugMessage with player token). Noted, not patched (debug only).

MOAD-0002 (intertangle): global mutable state typical for 1993 C++ game
  server, not a clean god-object coupling defect.
MOAD-0003 (leaked context): no thread_local usage found. CLEAN.
MOAD-0005 (thundering herd): single-threaded server, no cache races. CLEAN.
This commit is contained in:
russell@unturf.com 2026-03-31 13:00:38 -04:00
parent dbc058c155
commit 4641c3c60f
9 changed files with 686 additions and 0 deletions

View file

@ -0,0 +1,60 @@
--- a/include/WorldEventManager.h
+++ b/include/WorldEventManager.h
@@ -24,8 +24,8 @@
#include "common.h"
// System headers
+#include <bitset>
#include <map>
-#include <vector>
#include <algorithm>
// Common headers
@@ -48,22 +48,27 @@
plugin->Event(eventData);
}
- std::vector<bz_eEventType> HandledEvents;
+ std::bitset<bz_eLastEvent> handledEventBits;
bool HasEvent( bz_eEventType evt)
{
- return std::find(HandledEvents.begin(),HandledEvents.end(),evt) != HandledEvents.end();
+ return (evt >= 0 && evt < bz_eLastEvent) && handledEventBits.test(evt);
}
void AddEvent( bz_eEventType evt )
{
- if (std::find(HandledEvents.begin(),HandledEvents.end(),evt) == HandledEvents.end())
- HandledEvents.push_back(evt);
+ if (evt >= 0 && evt < bz_eLastEvent)
+ handledEventBits.set(evt);
}
void RemoveEvent( bz_eEventType evt )
{
- std::vector<bz_eEventType>::iterator itr = std::find(HandledEvents.begin(),HandledEvents.end(),evt);
-
- if ( itr!= HandledEvents.end())
- HandledEvents.erase(itr);
+ if (evt >= 0 && evt < bz_eLastEvent)
+ handledEventBits.reset(evt);
+ }
+
+ bool HasNoEvents() const
+ {
+ return handledEventBits.none();
}
};
--- a/src/bzfs/WorldEventManager.cxx
+++ b/src/bzfs/WorldEventManager.cxx
@@ -147,7 +147,7 @@
bz_EventHandler *handler = HandlerMap[plugin];
worldEventManager.removeEvent(eventType,handler);
- if (handler->HandledEvents.empty())
+ if (handler->HasNoEvents())
worldEventManager.removeHandler(handler);
return true;

BIN
defects/bzflag-0001/test/test Executable file

Binary file not shown.

View file

@ -0,0 +1,245 @@
// Unit test for bzflag-0001: bz_EventHandler::HasEvent() O(N) vector scan
// replaced with O(1) bitset lookup.
//
// Defect: bz_EventHandler::HasEvent() used std::find on a std::vector<bz_eEventType>
// to check if a handler handles a given event type. This is called inside
// WorldEventManager::callEvents() for every handler on every event fire,
// making it O(E * H) where E = handlers, H = events per handler.
//
// Fix: Replace std::vector<bz_eEventType> HandledEvents with
// std::bitset<bz_eLastEvent> handledEventBits. HasEvent/AddEvent/RemoveEvent
// become O(1) bitset operations.
#include <bitset>
#include <vector>
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cstdio>
// Minimal reproduction of BZFlag event types
enum bz_eEventType {
bz_eNullEvent = 0,
bz_eCaptureEvent,
bz_ePlayerDieEvent,
bz_ePlayerSpawnEvent,
bz_eZoneEntryEvent,
bz_eZoneExitEvent,
bz_ePlayerJoinEvent,
bz_ePlayerPartEvent,
bz_eRawChatMessageEvent,
bz_eFilteredChatMessageEvent,
bz_eUnknownSlashCommand,
bz_eGetPlayerSpawnPosEvent,
bz_eGetAutoTeamEvent,
bz_eAllowPlayer,
bz_eTickEvent,
bz_eGetWorldEvent,
bz_eGetPlayerInfoEvent,
bz_eAllowSpawn,
bz_eListServerUpdateEvent,
bz_eBanEvent,
bz_eHostBanModifyEvent,
bz_eKickEvent,
bz_eKillEvent,
bz_ePlayerPausedEvent,
bz_eMessageFilteredEvent,
bz_eGamePauseEvent,
bz_eGameResumeEvent,
bz_eGameStartEvent,
bz_eGameEndEvent,
bz_eSlashCommandEvent,
bz_ePlayerAuthEvent,
bz_eServerMsgEvent,
bz_eShotFiredEvent,
bz_ePlayerUpdateEvent,
bz_eNetDataSendEvent,
bz_eNetDataReceiveEvent,
bz_eLoggingEvent,
bz_eShotEndedEvent,
bz_eFlagTransferredEvent,
bz_eFlagGrabbedEvent,
bz_eFlagDroppedEvent,
bz_eAllowCTFCaptureEvent,
bz_eMsgDebugEvent,
bz_eNewNonPlayerConnection,
bz_ePluginLoaded,
bz_ePluginUnloaded,
bz_ePlayerScoreChanged,
bz_eTeamScoreChanged,
bz_eWorldFinalized,
bz_eReportFiledEvent,
bz_eBZDBChange,
bz_eGetPlayerMotto,
bz_eAllowConnection,
bz_eAllowFlagGrab,
bz_eAuthenticatonComplete,
bz_eServerAddPlayer,
bz_eAllowPollEvent,
bz_ePollStartEvent,
bz_ePollVoteEvent,
bz_ePollVetoEvent,
bz_ePollEndEvent,
bz_eComputeHandicapEvent,
bz_eBeginHandicapRefreshEvent,
bz_eEndHandicapRefreshEvent,
bz_eAutoPilotEvent,
bz_eMuteEvent,
bz_eUnmuteEvent,
bz_eServerShotFiredEvent,
bz_ePermissionModificationEvent,
bz_eAllowServerShotFiredEvent,
bz_ePlayerDeathFinalizedEvent,
bz_eLastEvent
};
// BEFORE: vector-based HasEvent (original defective code)
struct EventHandlerBefore {
std::vector<bz_eEventType> HandledEvents;
bool HasEvent(bz_eEventType evt) {
return std::find(HandledEvents.begin(), HandledEvents.end(), evt) != HandledEvents.end();
}
void AddEvent(bz_eEventType evt) {
if (std::find(HandledEvents.begin(), HandledEvents.end(), evt) == HandledEvents.end())
HandledEvents.push_back(evt);
}
void RemoveEvent(bz_eEventType evt) {
auto itr = std::find(HandledEvents.begin(), HandledEvents.end(), evt);
if (itr != HandledEvents.end())
HandledEvents.erase(itr);
}
bool IsEmpty() const { return HandledEvents.empty(); }
};
// AFTER: bitset-based HasEvent (patched code)
struct EventHandlerAfter {
std::bitset<bz_eLastEvent> handledEventBits;
bool HasEvent(bz_eEventType evt) {
return (evt >= 0 && evt < bz_eLastEvent) && handledEventBits.test(evt);
}
void AddEvent(bz_eEventType evt) {
if (evt >= 0 && evt < bz_eLastEvent)
handledEventBits.set(evt);
}
void RemoveEvent(bz_eEventType evt) {
if (evt >= 0 && evt < bz_eLastEvent)
handledEventBits.reset(evt);
}
bool HasNoEvents() const { return handledEventBits.none(); }
};
// Test correctness
void test_correctness() {
EventHandlerAfter h;
// Initially no events
assert(!h.HasEvent(bz_eTickEvent));
assert(!h.HasEvent(bz_eShotFiredEvent));
assert(h.HasNoEvents());
// Add events
h.AddEvent(bz_eTickEvent);
h.AddEvent(bz_eShotFiredEvent);
h.AddEvent(bz_ePlayerUpdateEvent);
assert(h.HasEvent(bz_eTickEvent));
assert(h.HasEvent(bz_eShotFiredEvent));
assert(h.HasEvent(bz_ePlayerUpdateEvent));
assert(!h.HasEvent(bz_eCaptureEvent));
assert(!h.HasNoEvents());
// Idempotent add
h.AddEvent(bz_eTickEvent);
assert(h.HasEvent(bz_eTickEvent));
// Remove
h.RemoveEvent(bz_eTickEvent);
assert(!h.HasEvent(bz_eTickEvent));
assert(h.HasEvent(bz_eShotFiredEvent));
// Remove all
h.RemoveEvent(bz_eShotFiredEvent);
h.RemoveEvent(bz_ePlayerUpdateEvent);
assert(h.HasNoEvents());
// Boundary: first and last valid events
h.AddEvent(bz_eNullEvent);
assert(h.HasEvent(bz_eNullEvent));
h.AddEvent((bz_eEventType)(bz_eLastEvent - 1));
assert(h.HasEvent((bz_eEventType)(bz_eLastEvent - 1)));
printf("PASS: correctness\n");
}
// Benchmark: simulate callEvents hot path
void test_performance() {
const int NUM_HANDLERS = 20; // typical plugin count
const int EVENTS_PER_HANDLER = 15; // events each handler registers for
const int ITERATIONS = 1000000; // event fires to simulate
// Prepare event types each handler cares about
std::vector<bz_eEventType> eventTypes;
for (int i = 0; i < EVENTS_PER_HANDLER && i < bz_eLastEvent; i++)
eventTypes.push_back((bz_eEventType)(i * 3 % bz_eLastEvent));
// Setup BEFORE handlers
std::vector<EventHandlerBefore> beforeHandlers(NUM_HANDLERS);
for (auto &h : beforeHandlers)
for (auto evt : eventTypes)
h.AddEvent(evt);
// Setup AFTER handlers
std::vector<EventHandlerAfter> afterHandlers(NUM_HANDLERS);
for (auto &h : afterHandlers)
for (auto evt : eventTypes)
h.AddEvent(evt);
// Benchmark BEFORE: simulate callEvents checking HasEvent for each handler
volatile int sink = 0;
auto t0 = std::chrono::high_resolution_clock::now();
for (int iter = 0; iter < ITERATIONS; iter++) {
bz_eEventType queryEvt = (bz_eEventType)(iter % bz_eLastEvent);
for (int h = 0; h < NUM_HANDLERS; h++) {
if (beforeHandlers[h].HasEvent(queryEvt))
sink++;
}
}
auto t1 = std::chrono::high_resolution_clock::now();
double before_ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
// Benchmark AFTER: same with bitset
volatile int sink2 = 0;
auto t2 = std::chrono::high_resolution_clock::now();
for (int iter = 0; iter < ITERATIONS; iter++) {
bz_eEventType queryEvt = (bz_eEventType)(iter % bz_eLastEvent);
for (int h = 0; h < NUM_HANDLERS; h++) {
if (afterHandlers[h].HasEvent(queryEvt))
sink2++;
}
}
auto t3 = std::chrono::high_resolution_clock::now();
double after_ms = std::chrono::duration<double, std::milli>(t3 - t2).count();
double ratio = before_ms / after_ms;
printf("BEFORE: %.1f ms\n", before_ms);
printf("AFTER: %.1f ms\n", after_ms);
printf("Ratio: %.1fx speedup\n", ratio);
// Patched version must be faster
assert(ratio > 2.0 && "Expected at least 2x speedup from bitset vs vector find");
printf("PASS: performance (%.1fx)\n", ratio);
}
int main() {
test_correctness();
test_performance();
printf("ALL TESTS PASSED\n");
return 0;
}

View file

@ -0,0 +1,104 @@
--- a/src/bzfs/AccessControlList.h
+++ b/src/bzfs/AccessControlList.h
@@ -12,6 +12,7 @@
// System headers
#include <vector>
+#include <unordered_set>
#include <string>
#include <string.h>
@@ -173,6 +174,15 @@
/* FIXME the AccessControlList assumes that 255 is a wildcard. it "should"
* include a cidr mask with each address. it's still useful as is, though
* see wildcard conversion occurs in convert().
+ *
+ * PERF NOTE: ban(), hostBan(), and idBan() each use std::find() on our
+ * vector to detect duplicates before inserting. When called from merge()
+ * which processes every entry in a master ban list, this makes ban list
+ * loading O(B^2) where B = number of bans. For large master ban lists
+ * (hundreds to thousands of entries from community servers) this is
+ * significant. Fix: maintain a parallel unordered_set index keyed on
+ * the ban identity (IP+CIDR for IP bans, hostpat for host bans, idpat
+ * for ID bans) to achieve O(1) duplicate detection.
*/
/** This class handles the lists of bans and hostbans. It has functions for
@@ -296,6 +306,15 @@
typedef std::vector<IdBanInfo> idBanList_t;
idBanList_t idBanList;
+ // O(1) duplicate detection indexes.
+ // Key for IP bans: (addr.s_addr, cidr) packed as uint64.
+ // Key for host bans: hostpat string.
+ // Key for ID bans: idpat string.
+ std::unordered_set<uint64_t> banIndex;
+ std::unordered_set<std::string> hostBanIndex;
+ std::unordered_set<std::string> idBanIndex;
+
+ static uint64_t banKey(in_addr addr, unsigned char cidr) { return ((uint64_t)addr.s_addr << 8) | cidr; }
std::string banFile;
private:
--- a/src/bzfs/AccessControlList.cxx
+++ b/src/bzfs/AccessControlList.cxx
@@ -41,9 +41,12 @@
{
BanInfo toban(ipAddr, bannedBy, period, cidr, fromMaster);
if (reason) toban.reason = reason;
- banList_t::iterator oldit = std::find(banList.begin(), banList.end(), toban);
- if (oldit != banList.end()) // IP already in list? -> replace
- *oldit = toban;
+ uint64_t key = banKey(ipAddr, cidr);
+ if (banIndex.count(key)) {
+ // IP already in list -> replace
+ banList_t::iterator oldit = std::find(banList.begin(), banList.end(), toban);
+ if (oldit != banList.end())
+ *oldit = toban;
+ }
else
+ {
+ banIndex.insert(key);
banList.push_back(toban);
+ }
}
@@ -93,9 +96,12 @@
{
HostBanInfo toban(hostpat, bannedBy, period,fromMaster);
if (reason) toban.reason = reason;
- hostBanList_t::iterator oldit = std::find(hostBanList.begin(), hostBanList.end(), toban);
- if (oldit != hostBanList.end())
- *oldit = toban;
+ if (hostBanIndex.count(hostpat)) {
+ hostBanList_t::iterator oldit = std::find(hostBanList.begin(), hostBanList.end(), toban);
+ if (oldit != hostBanList.end())
+ *oldit = toban;
+ }
else
+ {
+ hostBanIndex.insert(hostpat);
hostBanList.push_back(toban);
+ }
}
@@ -106,9 +112,12 @@
{
IdBanInfo toban(idpat, bannedBy, period, fromMaster);
if (reason) toban.reason = reason;
- idBanList_t::iterator oldit = std::find(idBanList.begin(), idBanList.end(), toban);
- if (oldit != idBanList.end())
- *oldit = toban;
+ if (idBanIndex.count(idpat)) {
+ idBanList_t::iterator oldit = std::find(idBanList.begin(), idBanList.end(), toban);
+ if (oldit != idBanList.end())
+ *oldit = toban;
+ }
else
+ {
+ idBanIndex.insert(idpat);
idBanList.push_back(toban);
+ }
}

BIN
defects/bzflag-0002/test/test Executable file

Binary file not shown.

View file

@ -0,0 +1,143 @@
// Unit test for bzflag-0002: AccessControlList ban/hostBan/idBan O(B^2) dedup
//
// Defect: ban(), hostBan(), and idBan() each use std::find() on a
// std::vector to detect duplicate bans before inserting. When called from
// merge() processing a master ban list, this makes ban list loading O(B^2).
//
// Fix: Maintain a parallel std::unordered_set index for O(1) duplicate
// detection. Only fall through to std::find for replacement when our
// index confirms a duplicate exists.
#include <vector>
#include <unordered_set>
#include <string>
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cstdio>
#include <cstdint>
#include <cstring>
// Minimal reproduction of BZFlag ban structures
struct in_addr_sim {
uint32_t s_addr;
};
struct BanInfo {
in_addr_sim addr;
unsigned char cidr;
std::string bannedBy;
std::string reason;
BanInfo(in_addr_sim a, unsigned char c) : addr(a), cidr(c) {}
bool operator==(const BanInfo &rhs) const {
return addr.s_addr == rhs.addr.s_addr && cidr == rhs.cidr;
}
};
// BEFORE: original O(B^2) pattern
struct BanListBefore {
std::vector<BanInfo> banList;
void ban(in_addr_sim addr, unsigned char cidr) {
BanInfo toban(addr, cidr);
auto oldit = std::find(banList.begin(), banList.end(), toban);
if (oldit != banList.end())
*oldit = toban;
else
banList.push_back(toban);
}
};
// AFTER: O(1) dedup with unordered_set index
struct BanListAfter {
std::vector<BanInfo> banList;
std::unordered_set<uint64_t> banIndex;
static uint64_t banKey(in_addr_sim addr, unsigned char cidr) {
return ((uint64_t)addr.s_addr << 8) | cidr;
}
void ban(in_addr_sim addr, unsigned char cidr) {
BanInfo toban(addr, cidr);
uint64_t key = banKey(addr, cidr);
if (banIndex.count(key)) {
auto oldit = std::find(banList.begin(), banList.end(), toban);
if (oldit != banList.end())
*oldit = toban;
} else {
banIndex.insert(key);
banList.push_back(toban);
}
}
};
void test_correctness() {
BanListAfter acl;
// Add unique bans
for (int i = 0; i < 100; i++) {
in_addr_sim a;
a.s_addr = (uint32_t)i;
acl.ban(a, 32);
}
assert(acl.banList.size() == 100);
// Add duplicate: should replace, not add
in_addr_sim dup;
dup.s_addr = 50;
acl.ban(dup, 32);
assert(acl.banList.size() == 100);
// Different CIDR = different ban
dup.s_addr = 50;
acl.ban(dup, 24);
assert(acl.banList.size() == 101);
printf("PASS: correctness\n");
}
void test_performance() {
const int NUM_BANS = 5000;
// Benchmark BEFORE
BanListBefore before;
auto t0 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < NUM_BANS; i++) {
in_addr_sim a;
a.s_addr = (uint32_t)i;
before.ban(a, 32);
}
auto t1 = std::chrono::high_resolution_clock::now();
double before_ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
// Benchmark AFTER
BanListAfter after;
auto t2 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < NUM_BANS; i++) {
in_addr_sim a;
a.s_addr = (uint32_t)i;
after.ban(a, 32);
}
auto t3 = std::chrono::high_resolution_clock::now();
double after_ms = std::chrono::duration<double, std::milli>(t3 - t2).count();
assert(before.banList.size() == (size_t)NUM_BANS);
assert(after.banList.size() == (size_t)NUM_BANS);
double ratio = before_ms / after_ms;
printf("BEFORE: %.1f ms (%d bans)\n", before_ms, NUM_BANS);
printf("AFTER: %.1f ms (%d bans)\n", after_ms, NUM_BANS);
printf("Ratio: %.1fx speedup\n", ratio);
assert(ratio > 5.0 && "Expected at least 5x speedup from hash index");
printf("PASS: performance (%.1fx)\n", ratio);
}
int main() {
test_correctness();
test_performance();
printf("ALL TESTS PASSED\n");
return 0;
}

View file

@ -0,0 +1,26 @@
--- a/src/bzfs/Permissions.cxx
+++ b/src/bzfs/Permissions.cxx
@@ -536,6 +536,7 @@
// return value is only needed for groupdb parsing, not for userdb.
bool parsePermissionString(const std::string &permissionString, PlayerAccessInfo &info)
{
+ std::set<std::string> customPermsSeen;
if (permissionString.length() < 1)
return false;
@@ -649,10 +650,14 @@
{
// Easy access
std::vector<std::string>& c = info.customPerms;
+ // Populate seen set on first custom perm encounter
+ if (customPermsSeen.empty() && !c.empty())
+ customPermsSeen.insert(c.begin(), c.end());
// Only store the custom permission if it doesn't exist, in order to prevent duplicates
- if (std::find(c.begin(), c.end(), word) == c.end())
+ if (customPermsSeen.find(word) == customPermsSeen.end())
{
+ customPermsSeen.insert(word);
c.push_back(word);
}
}

BIN
defects/bzflag-0003/test/test Executable file

Binary file not shown.

View file

@ -0,0 +1,108 @@
// Unit test for bzflag-0003: parsePermissionString customPerms dedup O(W*C)
//
// Defect: parsePermissionString uses std::find on customPerms vector to
// check for duplicates when adding custom permissions. When a group file
// has many custom permissions, this is O(W*C) per parse call.
//
// Fix: Use a std::set<std::string> shadow for O(log C) dedup lookups.
#include <vector>
#include <set>
#include <string>
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cstdio>
// BEFORE: vector find dedup
struct PermInfoBefore {
std::vector<std::string> customPerms;
void addCustomPerm(const std::string &word) {
if (std::find(customPerms.begin(), customPerms.end(), word) == customPerms.end())
customPerms.push_back(word);
}
};
// AFTER: set-backed dedup
struct PermInfoAfter {
std::vector<std::string> customPerms;
std::set<std::string> customPermsSeen;
void addCustomPerm(const std::string &word) {
if (customPermsSeen.find(word) == customPermsSeen.end()) {
customPermsSeen.insert(word);
customPerms.push_back(word);
}
}
};
void test_correctness() {
PermInfoAfter info;
info.addCustomPerm("CUSTOM_KICK");
info.addCustomPerm("CUSTOM_BAN");
info.addCustomPerm("CUSTOM_MUTE");
assert(info.customPerms.size() == 3);
// Duplicate should not be added
info.addCustomPerm("CUSTOM_KICK");
assert(info.customPerms.size() == 3);
// Order preserved
assert(info.customPerms[0] == "CUSTOM_KICK");
assert(info.customPerms[1] == "CUSTOM_BAN");
assert(info.customPerms[2] == "CUSTOM_MUTE");
printf("PASS: correctness\n");
}
void test_performance() {
const int NUM_PERMS = 2000;
// Generate unique perm names
std::vector<std::string> perms;
for (int i = 0; i < NUM_PERMS; i++)
perms.push_back("CUSTOM_PERM_" + std::to_string(i));
// Benchmark BEFORE
PermInfoBefore before;
auto t0 = std::chrono::high_resolution_clock::now();
for (int round = 0; round < 10; round++) {
before.customPerms.clear();
for (auto &p : perms)
before.addCustomPerm(p);
}
auto t1 = std::chrono::high_resolution_clock::now();
double before_ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
// Benchmark AFTER
PermInfoAfter after;
auto t2 = std::chrono::high_resolution_clock::now();
for (int round = 0; round < 10; round++) {
after.customPerms.clear();
after.customPermsSeen.clear();
for (auto &p : perms)
after.addCustomPerm(p);
}
auto t3 = std::chrono::high_resolution_clock::now();
double after_ms = std::chrono::duration<double, std::milli>(t3 - t2).count();
assert(before.customPerms.size() == (size_t)NUM_PERMS);
assert(after.customPerms.size() == (size_t)NUM_PERMS);
double ratio = before_ms / after_ms;
printf("BEFORE: %.1f ms (%d perms)\n", before_ms, NUM_PERMS);
printf("AFTER: %.1f ms (%d perms)\n", after_ms, NUM_PERMS);
printf("Ratio: %.1fx speedup\n", ratio);
assert(ratio > 3.0 && "Expected at least 3x speedup from set dedup");
printf("PASS: performance (%.1fx)\n", ratio);
}
int main() {
test_correctness();
test_performance();
printf("ALL TESTS PASSED\n");
return 0;
}