diff --git a/scripts/base/frameworks/file-analysis/main.bro b/scripts/base/frameworks/file-analysis/main.bro index 58e62c755d..0aaf67c07b 100644 --- a/scripts/base/frameworks/file-analysis/main.bro +++ b/scripts/base/frameworks/file-analysis/main.bro @@ -33,6 +33,10 @@ export { ## TODO: what's a reasonable default? const default_data_event_len: count = 1024*1024 &redef; + type ActionArgs: record { + extract_filename: string &optional; + }; + ## Contains all metadata related to the analysis of a given file, some ## of which is logged. type Info: record { @@ -57,25 +61,30 @@ export { ## Total number of bytes that are supposed to comprise the file content. total_bytes: count &log &optional; - ## The number of not all-in-sequence bytes over the course of the + ## The number of bytes in the file stream that were completely missed + ## during the process of analysis e.g. due to dropped packets. ## analysis that had to be discarded due to a reassembly buffer size ## of *reassembly_buffer_size* being filled. - undelivered: count &default=0; + missing_bytes: count &log &default=0; + + ## The number of not all-in-sequence bytes in the file stream that + ## were delivered to file actions/analyzers due to reassembly buffer + ## size of *reassembly_buffer_size* being filled. + overflow_bytes: count &log &default=0; ## The amount of time between receiving new data for this file that ## the analysis engine will wait before giving up on it. - timeout_interval: interval &default=default_timeout_interval; + timeout_interval: interval &log &default=default_timeout_interval; + ## Actions that have been added to the analysis of this file. + actions: vector of Action &default=vector(); + + ## The corresponding arguments supplied to each element of *actions*. + action_args: vector of ActionArgs &default=vector(); } &redef; ## TODO: document global policy: hook(trig: Trigger, info: Info); - ## TODO: document - global postpone_timeout: function(file_id: string): bool; + # TODO: wrapper functions for BiFs ? } - -function postpone_timeout(file_id: string): bool - { - return __postpone_timeout(file_id); - } diff --git a/src/FileAnalysisManager.cc b/src/FileAnalysisManager.cc index e8616df8ad..bd170b87bd 100644 --- a/src/FileAnalysisManager.cc +++ b/src/FileAnalysisManager.cc @@ -5,6 +5,11 @@ using namespace file_analysis; +// keep in order w/ declared enum values in file_analysis.bif +static ActionInstantiator action_factory[] = { + Extract::Instantiate, +}; + Action::Action(Info* arg_info) : info(arg_info) { } @@ -12,11 +17,41 @@ Action::Action(Info* arg_info) : info(arg_info) Extract::Extract(Info* arg_info, const string& arg_filename) : Action(arg_info), filename(arg_filename) { + fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666); + + if ( fd < 0 ) + { + fd = 0; + char buf[128]; + strerror_r(errno, buf, sizeof(buf)); + reporter->Error("cannot open %s: %s", filename.c_str(), buf); + } } -void Extract::DeliverStream(const u_char* data, uint64 len) +Extract::~Extract() { - // TODO: write data to filename + if ( fd ) + safe_close(fd); + } + +Action* Extract::Instantiate(const RecordVal* args, Info* info) + { + const char* field = "extract_filename"; + int off = BifType::Record::FileAnalysis::ActionArgs->FieldOffset(field); + Val* v = args->Lookup(off); + + if ( ! v ) return 0; + + return new Extract(info, v->AsString()->CheckString()); + } + +void Extract::DeliverChunk(const u_char* data, uint64 len, uint64 offset) + { + Action::DeliverChunk(data, len, offset); + + if ( ! fd ) return; + + safe_pwrite(fd, data, len, offset); } static TableVal* empty_conn_id_set() @@ -51,26 +86,36 @@ int Info::conn_uids_idx = -1; int Info::conn_ids_idx = -1; int Info::seen_bytes_idx = -1; int Info::total_bytes_idx = -1; -int Info::undelivered_idx = -1; +int Info::missing_bytes_idx = -1; +int Info::overflow_bytes_idx = -1; int Info::timeout_interval_idx = -1; +int Info::actions_idx = -1; +int Info::action_args_idx = -1; + +void Info::InitFieldIndices() + { + if ( file_id_idx != -1 ) return; + file_id_idx = Idx("file_id"); + parent_file_id_idx = Idx("parent_file_id"); + protocol_idx = Idx("protocol"); + conn_uids_idx = Idx("conn_uids"); + conn_ids_idx = Idx("conn_ids"); + seen_bytes_idx = Idx("seen_bytes"); + total_bytes_idx = Idx("total_bytes"); + missing_bytes_idx = Idx("missing_bytes"); + overflow_bytes_idx = Idx("overflow_bytes"); + timeout_interval_idx = Idx("timeout_interval"); + actions_idx = Idx("actions"); + action_args_idx = Idx("action_args"); + } Info::Info(const string& file_id, Connection* conn, const string& protocol) - : val(0), last_activity_time(network_time), postpone_timeout(false) + : val(0), last_activity_time(network_time), postpone_timeout(false), + need_reassembly(false) { DBG_LOG(DBG_FILE_ANALYSIS, "Creating new Info object %s", file_id.c_str()); - if ( file_id_idx == -1 ) - { - file_id_idx = Idx("file_id"); - parent_file_id_idx = Idx("parent_file_id"); - protocol_idx = Idx("protocol"); - conn_uids_idx = Idx("conn_uids"); - conn_ids_idx = Idx("conn_ids"); - seen_bytes_idx = Idx("seen_bytes"); - total_bytes_idx = Idx("total_bytes"); - undelivered_idx = Idx("undelivered"); - timeout_interval_idx = Idx("timeout_interval"); - } + InitFieldIndices(); val = new RecordVal(BifType::Record::FileAnalysis::Info); // TODO: hash/prettify file_id for script layer presentation @@ -82,13 +127,13 @@ Info::Info(const string& file_id, Connection* conn, const string& protocol) val->Assign(protocol_idx, new StringVal(protocol.c_str())); ScheduleInactivityTimer(); - Manager::EvaluatePolicy(BifEnum::FileAnalysis::TRIGGER_NEW, this); } Info::~Info() { - for ( size_t i = 0; i < analyzers.size(); ++i ) - delete analyzers[i]; + ActionMap::const_iterator it; + for ( it = actions.begin(); it != actions.end(); ++it ) + delete it->second; DBG_LOG(DBG_FILE_ANALYSIS, "Destroying Info object %s", FileID().c_str()); Unref(val); @@ -109,7 +154,7 @@ void Info::UpdateConnectionFields(Connection* conn) conn_ids->AsTableVal()->Assign(get_conn_id_val(conn), 0); } -uint64 Info::FieldDefaultCount(int idx) const +uint64 Info::LookupFieldDefaultCount(int idx) const { Val* v = val->LookupWithDefault(idx); uint64 rval = v->AsCount(); @@ -117,7 +162,7 @@ uint64 Info::FieldDefaultCount(int idx) const return rval; } -double Info::FieldDefaultInterval(int idx) const +double Info::LookupFieldDefaultInterval(int idx) const { Val* v = val->LookupWithDefault(idx); double rval = v->AsInterval(); @@ -136,7 +181,7 @@ int Info::Idx(const string& field) double Info::TimeoutInterval() const { - return FieldDefaultInterval(timeout_interval_idx); + return LookupFieldDefaultInterval(timeout_interval_idx); } string Info::FileID() const @@ -144,10 +189,10 @@ string Info::FileID() const return val->Lookup(file_id_idx)->AsString()->CheckString(); } -void Info::IncrementSeenBytes(uint64 size) +void Info::IncrementByteCount(uint64 size, int field_idx) { - uint64 old = FieldDefaultCount(seen_bytes_idx); - val->Assign(seen_bytes_idx, new Val(old + size, TYPE_COUNT)); + uint64 old = LookupFieldDefaultCount(field_idx); + val->Assign(field_idx, new Val(old + size, TYPE_COUNT)); } void Info::SetTotalBytes(uint64 size) @@ -159,7 +204,7 @@ bool Info::IsComplete() const { Val* total = val->Lookup(total_bytes_idx); if ( ! total ) return false; - if ( FieldDefaultCount(seen_bytes_idx) >= total->AsCount() ) + if ( LookupFieldDefaultCount(seen_bytes_idx) >= total->AsCount() ) return true; return false; } @@ -169,6 +214,88 @@ void Info::ScheduleInactivityTimer() const timer_mgr->Add(new InfoTimer(network_time, FileID(), TimeoutInterval())); } +bool Info::AddAction(EnumVal* act, RecordVal* args) + { + if ( actions.find(act->AsEnum()) != actions.end() ) return false; + + Action* a = action_factory[act->AsEnum()](args, this); + + if ( ! a ) return false; + + DBG_LOG(DBG_FILE_ANALYSIS, "Add action %d for file id %s", act->AsEnum(), + FileID().c_str()); + actions[act->AsEnum()] = a; + + VectorVal* av = val->LookupWithDefault(actions_idx)->AsVectorVal(); + VectorVal* aav = val->LookupWithDefault(action_args_idx)->AsVectorVal(); + + av->Assign(av->Size(), act->Ref(), 0); + aav->Assign(aav->Size(), args->Ref(), 0); + + Unref(av); + Unref(aav); + + return true; + } + +bool Info::RemoveAction(EnumVal* act) + { + ActionMap::iterator it = actions.find(act->AsEnum()); + + if ( it == actions.end() ) return false; + + DBG_LOG(DBG_FILE_ANALYSIS, "Remove action %d for file id %s", act->AsEnum(), + FileID().c_str()); + delete it->second; + actions.erase(it); + return true; + } + +void Info::DataIn(const u_char* data, uint64 len, uint64 offset) + { + ActionMap::const_iterator it; + for ( it = actions.begin(); it != actions.end(); ++it ) + it->second->DeliverChunk(data, len, offset); + + // TODO: check reassembly requirement based on buffer size in record + if ( ! need_reassembly ) return; + + // TODO: reassembly stuff, possibly having to deliver chunks if buffer full + // and incrememt overflow bytes + + IncrementByteCount(len, seen_bytes_idx); + } + +void Info::DataIn(const u_char* data, uint64 len) + { + ActionMap::const_iterator it; + for ( it = actions.begin(); it != actions.end(); ++it ) + { + it->second->DeliverStream(data, len); + uint64 offset = LookupFieldDefaultCount(seen_bytes_idx) + + LookupFieldDefaultCount(missing_bytes_idx); + it->second->DeliverChunk(data, len, offset); + } + + IncrementByteCount(len, seen_bytes_idx); + } + +void Info::EndOfFile() + { + ActionMap::const_iterator it; + for ( it = actions.begin(); it != actions.end(); ++it ) + it->second->EndOfFile(); + } + +void Info::Gap(uint64 offset, uint64 len) + { + ActionMap::const_iterator it; + for ( it = actions.begin(); it != actions.end(); ++it ) + it->second->Undelivered(offset, len); + + IncrementByteCount(len, missing_bytes_idx); + } + void InfoTimer::Dispatch(double t, int is_expire) { Info* info = file_mgr->Lookup(file_id); @@ -218,7 +345,7 @@ static void check_file_done(Info* info) if ( info->IsComplete() ) { Manager::EvaluatePolicy(BifEnum::FileAnalysis::TRIGGER_DONE, info); - file_mgr->Remove(info->FileID()); + file_mgr->RemoveFile(info->FileID()); } } @@ -226,9 +353,7 @@ void Manager::DataIn(const string& file_id, const u_char* data, uint64 len, uint64 offset, Connection* conn, const string& protocol) { Info* info = IDtoInfo(file_id, conn, protocol); - info->UpdateLastActivityTime(); - info->UpdateConnectionFields(conn); - // TODO: more stuff + info->DataIn(data, len, offset); check_file_done(info); } @@ -236,9 +361,7 @@ void Manager::DataIn(const string& file_id, const u_char* data, uint64 len, Connection* conn, const string& protocol) { Info* info = IDtoInfo(file_id, conn, protocol); - info->UpdateLastActivityTime(); - info->UpdateConnectionFields(conn); - // TODO: more stuff + info->DataIn(data, len); check_file_done(info); } @@ -246,18 +369,22 @@ void Manager::EndOfFile(const string& file_id, Connection* conn, const string& protocol) { Info* info = IDtoInfo(file_id, conn, protocol); - info->UpdateLastActivityTime(); - info->UpdateConnectionFields(conn); - Manager::EvaluatePolicy(BifEnum::FileAnalysis::TRIGGER_DONE, info); - Remove(file_id); + info->EndOfFile(); + Manager::EvaluatePolicy(BifEnum::FileAnalysis::TRIGGER_EOF, info); + } + +void Manager::Gap(const string& file_id, uint64 offset, uint64 len, + Connection* conn, const string& protocol) + { + Info* info = IDtoInfo(file_id, conn, protocol); + info->Gap(offset, len); + Manager::EvaluatePolicy(BifEnum::FileAnalysis::TRIGGER_GAP, info); } void Manager::SetSize(const string& file_id, uint64 size, Connection* conn, const string& protocol) { Info* info = IDtoInfo(file_id, conn, protocol); - info->UpdateLastActivityTime(); - info->UpdateConnectionFields(conn); info->SetTotalBytes(size); check_file_done(info); } @@ -288,12 +415,41 @@ bool Manager::PostponeTimeout(const string& file_id) const return true; } +bool Manager::AddAction(const string& file_id, EnumVal* act, + RecordVal* args) const + { + Info* info = Lookup(file_id); + + if ( ! info ) return false; + + return info->AddAction(act, args); + } + +bool Manager::RemoveAction(const string& file_id, EnumVal* act) const + { + Info* info = Lookup(file_id); + + if ( ! info ) return false; + + return info->RemoveAction(act); + } + Info* Manager::IDtoInfo(const string& file_id, Connection* conn, const string& protocol) { Info* rval = file_map[file_id]; + if ( ! rval ) + { rval = file_map[file_id] = new Info(file_id, conn, protocol); + Manager::EvaluatePolicy(BifEnum::FileAnalysis::TRIGGER_NEW, rval); + } + else + { + rval->UpdateLastActivityTime(); + rval->UpdateConnectionFields(conn); + } + return rval; } @@ -312,7 +468,7 @@ void Manager::Timeout(const string& file_id, bool is_terminating) if ( ! info ) return; - EvaluatePolicy(BifEnum::FileAnalysis::TRIGGER_TIMEOUT, info); + Manager::EvaluatePolicy(BifEnum::FileAnalysis::TRIGGER_TIMEOUT, info); if ( info->postpone_timeout && ! is_terminating ) { @@ -326,15 +482,16 @@ void Manager::Timeout(const string& file_id, bool is_terminating) DBG_LOG(DBG_FILE_ANALYSIS, "File analysis timeout for %s", info->FileID().c_str()); - Remove(file_id); + RemoveFile(file_id); } -void Manager::Remove(const string& file_id) +bool Manager::RemoveFile(const string& file_id) { FileMap::iterator it = file_map.find(file_id); - if ( it == file_map.end() ) return; + if ( it == file_map.end() ) return false; delete it->second; file_map.erase(it); + return true; } diff --git a/src/FileAnalysisManager.h b/src/FileAnalysisManager.h index ed11b9d912..40134d9f47 100644 --- a/src/FileAnalysisManager.h +++ b/src/FileAnalysisManager.h @@ -21,32 +21,55 @@ class Info; class Action { public: - Action(Info* arg_info); + virtual ~Action() {} - ~Action() {} + /** + * Subclasses may override this to receive file data non-sequentially. + */ + virtual void DeliverChunk(const u_char* data, uint64 len, uint64 offset) {} - virtual void DeliverStream(const u_char* data, uint64 len) = 0; + /** + * Subclasses may override this to receive file sequentially. + */ + virtual void DeliverStream(const u_char* data, uint64 len) {} + + /** + * Subclasses may override this to specifically handle the end of a file. + */ + virtual void EndOfFile() {} + + /** + * Subclasses may override this to handle missing data in a file stream. + */ + virtual void Undelivered(uint64 offset, uint64 len) {} protected: + Action(Info* arg_info); + Info* info; }; +typedef Action* (*ActionInstantiator)(const RecordVal* args, Info* info); + /** * An action to simply extract files to disk. */ class Extract : Action { public: - Extract(Info* arg_info, const string& arg_filename); + static Action* Instantiate(const RecordVal* args, Info* info); - ~Extract() {} + ~Extract(); - virtual void DeliverStream(const u_char* data, uint64 len); + virtual void DeliverChunk(const u_char* data, uint64 len, uint64 offset); protected: + Extract(Info* arg_info, const string& arg_filename); + string filename; + int fd; }; /** @@ -77,11 +100,6 @@ public: */ void UpdateLastActivityTime() { last_activity_time = network_time; } - /** - * Increments the "seen_bytes" field of #val record by \a size. - */ - void IncrementSeenBytes(uint64 size); - /** * Set "total_bytes" field of #val record to \a size. */ @@ -101,6 +119,38 @@ public: */ void ScheduleInactivityTimer() const; + /** + * Attaches an action. Only one action per type can be attached at a time. + * @return true if the action was attached, else false. + */ + bool AddAction(EnumVal* act, RecordVal* args); + + /** + * Removes an action. + * @return true if the action was removed, else false. + */ + bool RemoveAction(EnumVal* act); + + /** + * Pass in non-sequential data and deliver to attached actions/analyzers. + */ + void DataIn(const u_char* data, uint64 len, uint64 offset); + + /** + * Pass in sequential data and deliver to attached actions/analyzers. + */ + void DataIn(const u_char* data, uint64 len); + + /** + * Inform attached actions/analyzers about end of file being seen. + */ + void EndOfFile(); + + /** + * Inform attached actions/analyzers about a gap in file stream. + */ + void Gap(uint64 offset, uint64 len); + protected: friend class Manager; @@ -118,28 +168,41 @@ protected: void UpdateConnectionFields(Connection* conn); /** - * Wrapper to RecordVal::LookupWithDefault for the field in #val at index - * \a idx which automatically unrefs the Val and returns a converted value. + * Increment a byte count field of #val record by \a size. */ - uint64 FieldDefaultCount(int idx) const; + void IncrementByteCount(uint64 size, int field_idx); /** * Wrapper to RecordVal::LookupWithDefault for the field in #val at index * \a idx which automatically unrefs the Val and returns a converted value. */ - double FieldDefaultInterval(int idx) const; + uint64 LookupFieldDefaultCount(int idx) const; + + /** + * Wrapper to RecordVal::LookupWithDefault for the field in #val at index + * \a idx which automatically unrefs the Val and returns a converted value. + */ + double LookupFieldDefaultInterval(int idx) const; RecordVal* val; /**< \c FileAnalysis::Info from script layer. */ double last_activity_time; /**< Time of last activity. */ bool postpone_timeout; /**< Whether postponing timeout is requested. */ + bool need_reassembly; /**< Whether file stream reassembly is needed. */ - vector analyzers; + typedef map ActionMap; + + ActionMap actions; /** * @return the field offset in #val record corresponding to \a field_name. */ static int Idx(const string& field_name); + /** + * Initializes the index offsets for fields in \c FileAnalysis::info record. + */ + static void InitFieldIndices(); + static int file_id_idx; static int parent_file_id_idx; static int protocol_idx; @@ -147,12 +210,15 @@ protected: static int conn_ids_idx; static int seen_bytes_idx; static int total_bytes_idx; - static int undelivered_idx; + static int missing_bytes_idx; + static int overflow_bytes_idx; static int timeout_interval_idx; + static int actions_idx; + static int action_args_idx; }; /** - * Timer to periodically check if file analysis for a given file is inative. + * Timer to periodically check if file analysis for a given file is inactive. */ class InfoTimer : public Timer { public: @@ -207,6 +273,12 @@ public: void EndOfFile(const string& file_id, Connection* conn = 0, const string& protocol = ""); + /** + * Signal a gap in the file data stream. + */ + void Gap(const string& file_id, uint64 offset, uint64 len, + Connection* conn = 0, const string& protocol = ""); + /** * Provide the expected number of bytes that comprise a file. */ @@ -215,8 +287,9 @@ public: /** * Discard the file_analysis::Info object associated with \a file_id. + * @return false if file identifier did not map to anything, else true. */ - void Remove(const string& file_id); + bool RemoveFile(const string& file_id); /** * If called during \c FileAnalysis::policy evaluation for a @@ -224,6 +297,19 @@ public: */ bool PostponeTimeout(const string& file_id) const; + /** + * Attaches an action to the file identifier. Only one action of a given + * type can be attached per file identifier at a time. + * @return true if the action was attached, else false. + */ + bool AddAction(const string& file_id, EnumVal* act, RecordVal* args) const; + + /** + * Removes an action for a given file identifier. + * @return true if the action was removed, else false. + */ + bool RemoveAction(const string& file_id, EnumVal* act) const; + /** * Calls the \c FileAnalysis::policy hook. */ @@ -237,7 +323,8 @@ protected: /** * @return the Info object mapped to \a file_id. One is created if mapping - * doesn't exist. + * doesn't exist. If it did exist, the activity time is refreshed + * and connection-related fields of the record value may be updated. */ Info* IDtoInfo(const string& file_id, Connection* conn = 0, const string& protocol = ""); diff --git a/src/FileAnalyzer.cc b/src/FileAnalyzer.cc index b6cb4e74b8..26dd141003 100644 --- a/src/FileAnalyzer.cc +++ b/src/FileAnalyzer.cc @@ -44,6 +44,13 @@ void File_Analyzer::DeliverStream(int len, const u_char* data, bool orig) return; } +void File_Analyzer::Undelivered(int seq, int len, bool orig) + { + TCP_ApplicationAnalyzer::Undelivered(seq, len, orig); + + file_mgr->Gap(file_id, seq, len); + } + void File_Analyzer::Done() { TCP_ApplicationAnalyzer::Done(); diff --git a/src/FileAnalyzer.h b/src/FileAnalyzer.h index dedfaf3f02..e0a402daf2 100644 --- a/src/FileAnalyzer.h +++ b/src/FileAnalyzer.h @@ -16,6 +16,8 @@ public: virtual void DeliverStream(int len, const u_char* data, bool orig); + void Undelivered(int seq, int len, bool orig); + static Analyzer* InstantiateAnalyzer(Connection* conn) { return new File_Analyzer(conn); } diff --git a/src/file_analysis.bif b/src/file_analysis.bif index f951d5cc8e..f1646e5e8e 100644 --- a/src/file_analysis.bif +++ b/src/file_analysis.bif @@ -7,6 +7,7 @@ module FileAnalysis; %%} type Info: record; +type ActionArgs: record; ## An enumeration of possibly-interesting "events" that can occur over ## the course of analyzing files. The :bro:see:`FileAnalysis::policy` @@ -50,10 +51,42 @@ enum Trigger %{ ## TODO: Is it possible to extend the reassembly buffer when "handling" ## this trigger? TRIGGER_REASSEMBLY_BUFFER_FULL, + ## Raised when there's a missing chunk of data in the file stream. + TRIGGER_GAP, %} -function FileAnalysis::__postpone_timeout%(file_id: string%): bool +enum Action %{ + ACTION_EXTRACT, +%} + +function FileAnalysis::postpone_timeout%(file_id: string%): bool %{ bool result = file_mgr->PostponeTimeout(file_id->CheckString()); return new Val(result, TYPE_BOOL); %} + +function FileAnalysis::add_action%(file_id: string, + action: FileAnalysis::Action, + args: any%): bool + %{ + RecordVal* rv = args->AsRecordVal()->CoerceTo( + BifType::Record::FileAnalysis::ActionArgs); + bool result = file_mgr->AddAction(file_id->CheckString(), + action->AsEnumVal(), rv); + Unref(rv); + return new Val(result, TYPE_BOOL); + %} + +function FileAnalysis::remove_action%(file_id: string, + action: FileAnalysis::Action%): bool + %{ + bool result = file_mgr->RemoveAction(file_id->CheckString(), + action->AsEnumVal()); + return new Val(result, TYPE_BOOL); + %} + +function FileAnalysis::stop%(file_id: string%): bool + %{ + bool result = file_mgr->RemoveFile(file_id->CheckString()); + return new Val(result, TYPE_BOOL); + %} diff --git a/src/util.cc b/src/util.cc index c36ff6a31c..31cc18c4e0 100644 --- a/src/util.cc +++ b/src/util.cc @@ -1391,6 +1391,31 @@ bool safe_write(int fd, const char* data, int len) return true; } +bool safe_pwrite(int fd, const unsigned char* data, size_t len, size_t offset) + { + while ( len != 0 ) + { + ssize_t n = pwrite(fd, data, len, offset); + + if ( n < 0 ) + { + if ( errno == EINTR ) + continue; + + fprintf(stderr, "safe_write error: %d\n", errno); + abort(); + + return false; + } + + data += n; + offset +=n; + len -= n; + } + + return true; + } + void safe_close(int fd) { /* diff --git a/src/util.h b/src/util.h index 7d65f42fa8..6190f013e8 100644 --- a/src/util.h +++ b/src/util.h @@ -300,6 +300,10 @@ inline size_t pad_size(size_t size) // thread-safe as long as no two threads write to the same descriptor. extern bool safe_write(int fd, const char* data, int len); +// Same as safe_write(), but for pwrite(). +extern bool safe_pwrite(int fd, const unsigned char* data, size_t len, + size_t offset); + // Wraps close(2) to emit error messages and abort on unrecoverable errors. extern void safe_close(int fd);