Skip to content

Commit e965330

Browse files
committed
fix: perceptron save_to_json breaks on Windows (os.open can't fd-open a directory)
The POSIX shared-temp-squat hardening (_open_private_model_dir) called os.open(loc, O_RDONLY|O_DIRECTORY|O_NOFOLLOW) on the model *directory*, which fails on Windows with PermissionError [Errno 13] -- Windows cannot open a directory as a file descriptor via os.open. This broke every PerceptronTagger train/save (and its doctest) on windows-latest. The shared-temp squat is a POSIX concern: on Windows %TEMP% is per-user and ACL-protected, so there is no world-writable-/tmp exposure. Gate the hardened dir-fd path to POSIX and use a plain makedirs + write on Windows (the original pre-hardening behaviour). POSIX keeps the atomic O_NOFOLLOW dir-fd hardening unchanged; the squat regression tests are already POSIX-only.
1 parent df1bb4c commit e965330

1 file changed

Lines changed: 20 additions & 8 deletions

File tree

‎nltk/tag/perceptron.py‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -332,14 +332,26 @@ def train(self, sentences, save_loc=None, nr_iter=5):
332332
def save_to_json(self, lang="xxx", loc=None):
333333
if not loc:
334334
loc = self.save_dir
335-
# The default TRAINED_TAGGER_PATH is the system temp dir -- shared and
336-
# world-writable on Linux -- and the save dir is a *guessable* name in
337-
# it, so a local attacker can pre-plant or race a symlink at ``loc``. A
338-
# plain ``islink`` pre-check is non-atomic (TOCTOU) and misses it once
339-
# created; ``_open_private_model_dir`` instead creates/re-opens the leaf
340-
# atomically with O_NOFOLLOW|O_DIRECTORY and verifies it is a real,
341-
# user-owned, non-world-writable directory, returning a pinned fd
342-
# (CWE-59/377/378).
335+
# On POSIX the default TRAINED_TAGGER_PATH is a shared, world-writable
336+
# temp dir (/tmp) and the save dir is a *guessable* name in it, so a
337+
# local attacker can pre-plant or race a symlink at ``loc``. A plain
338+
# ``islink`` pre-check is non-atomic (TOCTOU) and misses it once created;
339+
# ``_open_private_model_dir`` instead creates/re-opens the leaf atomically
340+
# with O_NOFOLLOW|O_DIRECTORY, verifies it is a real, user-owned,
341+
# non-world-writable directory, and returns a pinned fd we write relative
342+
# to (CWE-59/377/378).
343+
#
344+
# On Windows ``%TEMP%`` is per-user and ACL-protected (no such squat), and
345+
# ``os.open`` cannot open a directory as a descriptor there, so use a
346+
# plain create + write.
347+
if os.name != "posix":
348+
os.makedirs(loc, exist_ok=True)
349+
_authorize_private_dir(loc)
350+
for param, json_file in zip(self.encode_json_obj(), self.param_files(lang)):
351+
with open(path_join(loc, json_file), "w") as fout:
352+
json.dump(param, fout)
353+
return
354+
343355
dir_fd = _open_private_model_dir(loc)
344356
try:
345357
_authorize_private_dir(loc)

0 commit comments

Comments
 (0)