fix: harden native workspace root capabilities
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
#include <string>
|
||||
#include <stdint.h>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <algorithm>
|
||||
#ifdef __APPLE__
|
||||
#include <sys/types.h>
|
||||
@@ -19,21 +21,14 @@
|
||||
namespace {
|
||||
static const uint64_t MAGIC=0x5448545746534154ULL;
|
||||
static const char OWNER_TOKEN = 0;
|
||||
struct Handle { uint64_t magic; const void* owner; int fd; bool directory; bool closed; unsigned borrows; };
|
||||
static std::unordered_set<Handle*> handles;
|
||||
static void finalize(napi_env, void* data, void*) {
|
||||
auto *h=static_cast<Handle*>(data);
|
||||
if (!h) return;
|
||||
if (handles.erase(h) && !h->closed) { h->closed=true; int rc = ::close(h->fd); (void)rc; }
|
||||
delete h;
|
||||
}
|
||||
static bool getHandle(napi_env env,napi_value v,Handle** out) {
|
||||
void *p=nullptr;
|
||||
if (napi_get_value_external(env,v,&p)!=napi_ok || !p) return false;
|
||||
auto *h=static_cast<Handle*>(p);
|
||||
if (handles.find(h)==handles.end() || h->magic!=MAGIC || h->owner!=&OWNER_TOKEN || h->closed) return false;
|
||||
*out=h; return true;
|
||||
}
|
||||
struct Handle { uint64_t magic; const void* owner; napi_env env; int fd; bool directory; bool closed; unsigned borrows; };
|
||||
struct EnvState { std::unordered_set<Handle*> handles; std::unordered_set<Handle*> all; std::mutex mutex; };
|
||||
static std::mutex statesMutex;
|
||||
static std::unordered_map<napi_env, EnvState*> states;
|
||||
static EnvState* stateFor(napi_env env) { std::lock_guard<std::mutex> g(statesMutex); auto it=states.find(env); if(it!=states.end()) return it->second; auto *s=new EnvState; states.emplace(env,s); return s; }
|
||||
static void finalize(napi_env env, void* data, void*) { auto *h=static_cast<Handle*>(data); if (!h) return; auto *st=stateFor(env); { std::lock_guard<std::mutex> g(st->mutex); st->handles.erase(h); st->all.erase(h); if (!h->closed) { h->closed=true; (void)::close(h->fd); } } delete h; }
|
||||
enum HandleResult { HANDLE_OK, HANDLE_CLOSED, HANDLE_ARGUMENT };
|
||||
static HandleResult getHandle(napi_env env,napi_value v,Handle** out) { void *p=nullptr; if (napi_get_value_external(env,v,&p)!=napi_ok || !p) return HANDLE_ARGUMENT; auto *h=static_cast<Handle*>(p); auto *st=stateFor(env); std::lock_guard<std::mutex> g(st->mutex); if (st->all.find(h)==st->all.end() || h->magic!=MAGIC || h->owner!=&OWNER_TOKEN || h->env!=env) return HANDLE_ARGUMENT; if (h->closed || st->handles.find(h)==st->handles.end()) return HANDLE_CLOSED; *out=h; return HANDLE_OK; }
|
||||
static const char* errnoName(int e) {
|
||||
switch(e) {
|
||||
case EINVAL:return "EINVAL"; case EBADF:return "EBADF"; case EEXIST:return "EEXIST";
|
||||
@@ -59,6 +54,7 @@ static napi_value error(napi_env env,const char* syscall,int e,const char* force
|
||||
return err;
|
||||
}
|
||||
static napi_value fail(napi_env env,const char*s,int e,const char* forced=nullptr){ napi_value x=error(env,s,e,forced); napi_throw(env,x); return nullptr; }
|
||||
static bool requireHandle(napi_env env,napi_value v,Handle** out,const char* syscall) { auto r=getHandle(env,v,out); if(r==HANDLE_OK) return true; napi_throw(env,error(env,syscall,EINVAL,r==HANDLE_CLOSED?"ERR_WORKSPACE_FS_AT_HANDLE_CLOSED":"ERR_WORKSPACE_FS_AT_ARGUMENT")); return false; }
|
||||
static bool str(napi_env env,napi_value v,std::string& out) {
|
||||
size_t n=0; if(napi_get_value_string_utf8(env,v,nullptr,0,&n)!=napi_ok) return false;
|
||||
out.resize(n); size_t got=0; if(napi_get_value_string_utf8(env,v,out.data(),n+1,&got)!=napi_ok) return false; out.resize(got); return true;
|
||||
@@ -68,27 +64,16 @@ static bool validComponent(const std::string& s) {
|
||||
}
|
||||
static int openRetry(int dir,const char *name,int flags,mode_t mode) { int fd; do {fd=dir<0 ? ::open(name,flags,mode) : ::openat(dir,name,flags,mode);} while(fd<0&&errno==EINTR); return fd; }
|
||||
static int statRetry(int fd, struct stat *st) { int rc; do {rc=::fstat(fd,st);} while(rc<0&&errno==EINTR); return rc; }
|
||||
static napi_value statObj(napi_env env,const struct stat& st) {
|
||||
napi_value o,n; napi_create_object(env,&o);
|
||||
napi_create_bigint_uint64(env,(uint64_t)st.st_dev,&n); napi_set_named_property(env,o,"device",n);
|
||||
napi_create_bigint_uint64(env,(uint64_t)st.st_ino,&n); napi_set_named_property(env,o,"inode",n);
|
||||
napi_create_uint32(env,(uint32_t)st.st_mode,&n); napi_set_named_property(env,o,"mode",n);
|
||||
napi_create_uint32(env,(uint32_t)st.st_uid,&n); napi_set_named_property(env,o,"uid",n);
|
||||
napi_create_uint32(env,(uint32_t)st.st_gid,&n); napi_set_named_property(env,o,"gid",n);
|
||||
napi_create_bigint_uint64(env,(uint64_t)st.st_nlink,&n); napi_set_named_property(env,o,"nlink",n); return o;
|
||||
}
|
||||
static napi_value result(napi_env env,int fd,bool dir,const struct stat&st) {
|
||||
auto*h=new Handle{MAGIC,&OWNER_TOKEN,fd,dir,false,0}; handles.insert(h);
|
||||
napi_value e,o,s; napi_create_external(env,h,finalize,nullptr,&e); napi_create_object(env,&o);
|
||||
napi_set_named_property(env,o,"handle",e); s=statObj(env,st); napi_set_named_property(env,o,"openedStat",s); return o;
|
||||
}
|
||||
static bool setStat(napi_env env,napi_value o,const char *name,uint64_t value,bool big) { napi_value n; napi_status rc=big?napi_create_bigint_uint64(env,value,&n):napi_create_uint32(env,(uint32_t)value,&n); if(rc!=napi_ok) return false; return napi_set_named_property(env,o,name,n)==napi_ok; }
|
||||
static napi_value statObj(napi_env env,const struct stat& st) { napi_value o; if(napi_create_object(env,&o)!=napi_ok || !setStat(env,o,"device",(uint64_t)st.st_dev,true) || !setStat(env,o,"inode",(uint64_t)st.st_ino,true) || !setStat(env,o,"mode",(uint64_t)st.st_mode,false) || !setStat(env,o,"uid",(uint64_t)st.st_uid,false) || !setStat(env,o,"gid",(uint64_t)st.st_gid,false) || !setStat(env,o,"nlink",(uint64_t)st.st_nlink,true)) { napi_throw_error(env,"ERR_WORKSPACE_FS_AT_ARGUMENT","native result conversion failed"); return nullptr; } return o; }
|
||||
static napi_value result(napi_env env,int fd,bool dir,const struct stat&st) { auto*h=new Handle{MAGIC,&OWNER_TOKEN,env,fd,dir,false,0}; auto *stt=stateFor(env); { std::lock_guard<std::mutex> g(stt->mutex); stt->handles.insert(h); stt->all.insert(h); } napi_value e,o,so; if(napi_create_external(env,h,finalize,nullptr,&e)!=napi_ok || napi_create_object(env,&o)!=napi_ok || napi_set_named_property(env,o,"handle",e)!=napi_ok || !(so=statObj(env,st)) || napi_set_named_property(env,o,"openedStat",so)!=napi_ok) { { std::lock_guard<std::mutex> g(stt->mutex); h->closed=true; stt->handles.erase(h); } (void)::close(fd); napi_throw_error(env,"ERR_WORKSPACE_FS_AT_ARGUMENT","native result conversion failed"); return nullptr; } return o; }
|
||||
static bool getInput(napi_env env,napi_callback_info info,napi_value *a,size_t *argc) { return napi_get_cb_info(env,info,argc,a,nullptr,nullptr)==napi_ok; }
|
||||
static napi_value openatFn(napi_env env,napi_callback_info info) {
|
||||
size_t argc=1; napi_value a[1]; if(!getInput(env,info,a,&argc)||argc!=1)return fail(env,"openat",EINVAL);
|
||||
napi_value pv,nv,kv,mv; if(napi_get_named_property(env,a[0],"parent",&pv)!=napi_ok||napi_get_named_property(env,a[0],"name",&nv)!=napi_ok||napi_get_named_property(env,a[0],"kind",&kv)!=napi_ok||napi_get_named_property(env,a[0],"createMode",&mv)!=napi_ok)return fail(env,"openat",EINVAL);
|
||||
std::string name,kind; if(!str(env,nv,name)||!str(env,kv,kind))return fail(env,"openat",EINVAL); int32_t mode;if(napi_get_value_int32(env,mv,&mode)!=napi_ok)return fail(env,"openat",EINVAL);
|
||||
bool root=name=="/"; if(!root&&!validComponent(name))return fail(env,"openat",EINVAL); Handle*ph=nullptr;
|
||||
if(root){if(kind!="directory"||mode!=0)return fail(env,"openat",EINVAL);} else if(!getHandle(env,pv,&ph)||!ph->directory)return fail(env,"openat",EBADF);
|
||||
if(root){if(kind!="directory"||mode!=0)return fail(env,"openat",EINVAL);} else if(!requireHandle(env,pv,&ph,"openat")||!ph->directory)return fail(env,"openat",EBADF);
|
||||
int fd=-1;
|
||||
if(root) fd=openRetry(-1,"/",O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW,0);
|
||||
else if(kind=="directory"&&mode==0) { fd=openRetry(ph->fd,name.c_str(),O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW,0); }
|
||||
@@ -100,41 +85,16 @@ static napi_value openatFn(napi_env env,napi_callback_info info) {
|
||||
if(kind=="regular_lock" && (!S_ISREG(st.st_mode)||(st.st_mode&0777)!=0600||st.st_uid!=(uid_t)::geteuid()||st.st_nlink!=1)) {::close(fd);return fail(env,"openat",EPERM);}
|
||||
return result(env,fd,kind=="directory",st);
|
||||
}
|
||||
static napi_value mkdiratFn(napi_env env,napi_callback_info info){size_t n=3;napi_value a[3];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h;std::string s;int32_t m;if(n!=3||!getHandle(env,a[0],&h)||!h->directory||!str(env,a[1],s)||napi_get_value_int32(env,a[2],&m)!=napi_ok||m!=0700||!validComponent(s))return fail(env,"mkdirat",EINVAL);int rc;do{rc=::mkdirat(h->fd,s.c_str(),0700);}while(rc<0&&errno==EINTR);if(rc<0)return fail(env,"mkdirat",errno);return nullptr;}
|
||||
static napi_value fstatatFn(napi_env env,napi_callback_info info){size_t n=2;napi_value a[2];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h;std::string s;if(n!=2||!getHandle(env,a[0],&h)||!h->directory||!str(env,a[1],s)||!validComponent(s))return fail(env,"fstatat",EINVAL);struct stat st;int rc;do{rc=::fstatat(h->fd,s.c_str(),&st,AT_SYMLINK_NOFOLLOW);}while(rc<0&&errno==EINTR);if(rc<0)return fail(env,"fstatat",errno);return statObj(env,st);}
|
||||
static napi_value fsyncFn(napi_env env,napi_callback_info info){size_t n=1;napi_value a[1];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h;if(n!=1||!getHandle(env,a[0],&h)||!h->directory)return fail(env,"fsync",EBADF);int rc;do{rc=::fsync(h->fd);}while(rc<0&&errno==EINTR);
|
||||
static napi_value mkdiratFn(napi_env env,napi_callback_info info){size_t n=3;napi_value a[3];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h;std::string s;int32_t m;if(n!=3||!requireHandle(env,a[0],&h,"workspace-fs-at")||!h->directory||!str(env,a[1],s)||napi_get_value_int32(env,a[2],&m)!=napi_ok||m!=0700||!validComponent(s))return fail(env,"mkdirat",EINVAL);int rc;do{rc=::mkdirat(h->fd,s.c_str(),0700);}while(rc<0&&errno==EINTR);if(rc<0)return fail(env,"mkdirat",errno);return nullptr;}
|
||||
static napi_value fstatatFn(napi_env env,napi_callback_info info){size_t n=2;napi_value a[2];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h;std::string s;if(n!=2||!requireHandle(env,a[0],&h,"workspace-fs-at")||!h->directory||!str(env,a[1],s)||!validComponent(s))return fail(env,"fstatat",EINVAL);struct stat st;int rc;do{rc=::fstatat(h->fd,s.c_str(),&st,AT_SYMLINK_NOFOLLOW);}while(rc<0&&errno==EINTR);if(rc<0)return fail(env,"fstatat",errno);return statObj(env,st);}
|
||||
static napi_value fsyncFn(napi_env env,napi_callback_info info){size_t n=1;napi_value a[1];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h;if(n!=1||!requireHandle(env,a[0],&h,"workspace-fs-at")||!h->directory)return fail(env,"fsync",EBADF);int rc;do{rc=::fsync(h->fd);}while(rc<0&&errno==EINTR);
|
||||
#ifdef __APPLE__
|
||||
if(rc<0&&(errno==EINVAL||errno==ENOTSUP)){int frc;do{frc=::fcntl(h->fd,F_FULLFSYNC);}while(frc<0&&errno==EINTR);if(frc==0)return nullptr;rc=frc;}
|
||||
#endif
|
||||
if(rc<0)return fail(env,"fsync",errno);return nullptr;}
|
||||
static napi_value closeFn(napi_env env,napi_callback_info info){size_t n=1;napi_value a[1];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h;if(n!=1||!getHandle(env,a[0],&h))return fail(env,"close",EBADF);if(h->borrows)return fail(env,"close",EBUSY);h->closed=true;handles.erase(h);int rc=::close(h->fd);if(rc<0){int e=errno;if(e==EINTR)return fail(env,"close",e,"ERR_WORKSPACE_FS_AT_CLOSE_UNCERTAIN");return fail(env,"close",e);}return nullptr;}
|
||||
static napi_value withFdFn(napi_env env,napi_callback_info info){size_t n=2;napi_value a[2];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h; napi_valuetype t;if(n!=2||!getHandle(env,a[0],&h)||napi_typeof(env,a[1],&t)!=napi_ok||t!=napi_function)return fail(env,"borrow",EINVAL);h->borrows++;napi_value argv; napi_create_int32(env,h->fd,&argv); napi_value out; napi_value global; napi_get_global(env, &global); napi_status rc=napi_call_function(env, global,a[1],1,&argv,&out);h->borrows--;if(rc!=napi_ok)return nullptr;return out;}
|
||||
static napi_value fdNumberForSynchronousBorrowFn(napi_env env,napi_callback_info info){ size_t n=1; napi_value a[1]; napi_get_cb_info(env,info,&n,a,nullptr,nullptr); Handle*h; if(n!=1||!getHandle(env,a[0],&h)) return fail(env,"fcntl",EBADF); napi_value out; napi_create_int32(env,h->fd,&out); return out; }
|
||||
static napi_value fcntlFn(napi_env env,napi_callback_info info){
|
||||
size_t n=2; napi_value a[2]; napi_get_cb_info(env,info,&n,a,nullptr,nullptr); Handle*h; std::string op;
|
||||
if(n!=2||!getHandle(env,a[0],&h)||!str(env,a[1],op)||h->directory) return fail(env,"fcntl",EINVAL);
|
||||
if(op=="unlock") {
|
||||
#ifdef F_OFD_SETLK
|
||||
struct flock lk{}; lk.l_type=F_UNLCK; lk.l_whence=SEEK_SET; if(::fcntl(h->fd,F_OFD_SETLK,&lk)<0) return fail(env,"fcntl",errno);
|
||||
#else
|
||||
if(::flock(h->fd,LOCK_UN)<0) return fail(env,"fcntl",errno);
|
||||
#endif
|
||||
napi_value out; napi_create_string_utf8(env,"available",NAPI_AUTO_LENGTH,&out); return out;
|
||||
}
|
||||
if(op=="probe-exclusive-nonblocking" || op=="hold-exclusive") {
|
||||
#ifdef F_OFD_SETLK
|
||||
struct flock lk{}; lk.l_type=F_WRLCK; lk.l_whence=SEEK_SET; int rc=::fcntl(h->fd,F_OFD_SETLK,&lk);
|
||||
if(rc==0) { if(op=="probe-exclusive-nonblocking") { lk.l_type=F_UNLCK; ::fcntl(h->fd,F_OFD_SETLK,&lk); } napi_value out; napi_create_string_utf8(env,op=="hold-exclusive"?"held":"available",NAPI_AUTO_LENGTH,&out); return out; }
|
||||
#else
|
||||
int rc=::flock(h->fd,LOCK_EX|LOCK_NB);
|
||||
if(rc==0) { if(op=="probe-exclusive-nonblocking") ::flock(h->fd,LOCK_UN); napi_value out; napi_create_string_utf8(env,op=="hold-exclusive"?"held":"available",NAPI_AUTO_LENGTH,&out); return out; }
|
||||
#endif
|
||||
if(errno==EWOULDBLOCK||errno==EAGAIN||errno==EACCES) { napi_value out; napi_create_string_utf8(env,"held",NAPI_AUTO_LENGTH,&out); return out; }
|
||||
return fail(env,"fcntl",errno);
|
||||
}
|
||||
return fail(env,"fcntl",EINVAL);
|
||||
}
|
||||
static napi_value duplicateForChildStdioFn(napi_env env,napi_callback_info info){size_t n=4;napi_value a[4];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*w,*r;int32_t wf,rf;if(n!=4||!getHandle(env,a[0],&w)||!getHandle(env,a[1],&r)||w->directory||!r->directory||napi_get_value_int32(env,a[2],&wf)!=napi_ok||napi_get_value_int32(env,a[3],&rf)!=napi_ok||wf<0||rf<0)return fail(env,"dup2",EINVAL);if(w->borrows||r->borrows)return fail(env,"dup2",EBUSY);int rc;do{rc=::dup2(w->fd,wf);}while(rc<0&&errno==EINTR);if(rc<0)return fail(env,"dup2",errno);do{rc=::dup2(r->fd,rf);}while(rc<0&&errno==EINTR);if(rc<0)return fail(env,"dup2",errno);return nullptr;}
|
||||
static napi_value init(napi_env env,napi_value exports){napi_property_descriptor pub[]={{"openat",0,openatFn,0,0,0,napi_enumerable,0},{"mkdirat",0,mkdiratFn,0,0,0,napi_enumerable,0},{"fstatat",0,fstatatFn,0,0,0,napi_enumerable,0},{"fsyncDirectory",0,fsyncFn,0,0,0,napi_enumerable,0},{"close",0,closeFn,0,0,0,napi_enumerable,0}};napi_define_properties(env,exports,5,pub);napi_property_descriptor priv[]={{"withFd",0,withFdFn,0,0,0,napi_default,0},{"duplicateForChildStdio",0,duplicateForChildStdioFn,0,0,0,napi_default,0},{"fdNumberForSynchronousBorrow",0,fdNumberForSynchronousBorrowFn,0,0,0,napi_default,0},{"fcntl",0,fcntlFn,0,0,0,napi_default,0}};napi_define_properties(env,exports,4,priv);return exports;}
|
||||
static napi_value closeFn(napi_env env,napi_callback_info info){ size_t n=1; napi_value a[1]; napi_get_cb_info(env,info,&n,a,nullptr,nullptr); Handle*h=nullptr; if(n!=1) return fail(env,"close",EINVAL,"ERR_WORKSPACE_FS_AT_ARGUMENT"); auto hr=getHandle(env,a[0],&h); if(hr!=HANDLE_OK) return fail(env,"close",EINVAL,hr==HANDLE_CLOSED?"ERR_WORKSPACE_FS_AT_HANDLE_CLOSED":"ERR_WORKSPACE_FS_AT_ARGUMENT"); if(h->borrows) return fail(env,"close",EBUSY); auto *st=stateFor(env); { std::lock_guard<std::mutex> g(st->mutex); h->closed=true; st->handles.erase(h); } int rc=::close(h->fd); if(rc<0){ int e=errno; if(e==EINTR)return fail(env,"close",e,"ERR_WORKSPACE_FS_AT_CLOSE_UNCERTAIN"); return fail(env,"close",e); } return nullptr; }
|
||||
static napi_value withFdFn(napi_env env,napi_callback_info info){size_t n=2;napi_value a[2];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h; napi_valuetype t;if(n!=2||!requireHandle(env,a[0],&h,"workspace-fs-at")||napi_typeof(env,a[1],&t)!=napi_ok||t!=napi_function)return fail(env,"borrow",EINVAL);h->borrows++;napi_value argv; napi_create_int32(env,h->fd,&argv); napi_value out; napi_value global; napi_get_global(env, &global); napi_status rc=napi_call_function(env, global,a[1],1,&argv,&out);h->borrows--;if(rc!=napi_ok)return nullptr;return out;}
|
||||
static napi_value duplicateForChildStdioFn(napi_env env,napi_callback_info info){size_t n=4;napi_value a[4];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*w,*r;int32_t wf,rf;if(n!=4||!requireHandle(env,a[0],&w,"dup2")||!requireHandle(env,a[1],&r,"dup2")||w->directory||!r->directory||napi_get_value_int32(env,a[2],&wf)!=napi_ok||napi_get_value_int32(env,a[3],&rf)!=napi_ok||wf<0||rf<0)return fail(env,"dup2",EINVAL);if(w->borrows||r->borrows)return fail(env,"dup2",EBUSY);int rc;do{rc=::dup2(w->fd,wf);}while(rc<0&&errno==EINTR);if(rc<0)return fail(env,"dup2",errno);do{rc=::dup2(r->fd,rf);}while(rc<0&&errno==EINTR);if(rc<0)return fail(env,"dup2",errno);return nullptr;}
|
||||
static napi_value init(napi_env env,napi_value exports){napi_property_descriptor pub[]={{"openat",0,openatFn,0,0,0,napi_enumerable,0},{"mkdirat",0,mkdiratFn,0,0,0,napi_enumerable,0},{"fstatat",0,fstatatFn,0,0,0,napi_enumerable,0},{"fsyncDirectory",0,fsyncFn,0,0,0,napi_enumerable,0},{"close",0,closeFn,0,0,0,napi_enumerable,0}};napi_define_properties(env,exports,5,pub);napi_property_descriptor priv[]={{"withFd",0,withFdFn,0,0,0,napi_default,0},{"duplicateForChildStdio",0,duplicateForChildStdioFn,0,0,0,napi_default,0}};napi_define_properties(env,exports,2,priv);return exports;}
|
||||
}
|
||||
NAPI_MODULE(NODE_GYP_MODULE_NAME,init)
|
||||
|
||||
Reference in New Issue
Block a user