Skip to content

Commit 2d7e971

Browse files
gh-152275: Add integer overflow guards to the curses chtype and color-pair packing path
curses.color_pair() now raises OverflowError for a pair number too large to be packed, instead of silently masking it to a different pair. The attr argument of the character-cell and attribute methods (addch, addstr, attron, attrset and others) now goes through the checked attr converter, so an out-of-range or non-integer attribute is rejected rather than silently truncated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 05679f3 commit 2d7e971

5 files changed

Lines changed: 137 additions & 103 deletions

File tree

Doc/library/curses.rst

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,13 @@ The module :mod:`!curses` defines the following functions:
139139
.. function:: color_pair(pair_number)
140140

141141
Return the attribute value for displaying text in the specified color pair.
142-
Only the first 256 color pairs are supported. This
143-
attribute value can be combined with :const:`A_STANDOUT`, :const:`A_REVERSE`,
144-
and the other :const:`!A_\*` attributes. :func:`pair_number` is the counterpart
145-
to this function.
142+
Only color pairs that fit in the color-pair field of the returned value can
143+
be represented (usually the first 256); a larger *pair_number* raises
144+
:exc:`OverflowError` rather than being silently masked to a different pair.
145+
Use :meth:`~window.color_set` or :meth:`~window.attr_set` to display higher
146+
pairs. This attribute value can be combined with :const:`A_STANDOUT`,
147+
:const:`A_REVERSE`, and the other :const:`!A_\*` attributes.
148+
:func:`pair_number` is the counterpart to this function.
146149

147150

148151
.. function:: curs_set(visibility)

Lib/test/test_curses.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,10 @@ def test_argument_errors(self):
784784
# A character argument must be an int, a byte or a one-element string.
785785
self.assertRaises(TypeError, win.addch, [])
786786
self.assertRaises(OverflowError, win.addch, 2**64)
787+
# The attribute argument is rejected, not truncated, when out of range.
788+
self.assertRaises(OverflowError, win.addch, 'a', 2**64)
789+
self.assertRaises(OverflowError, win.addstr, 'a', 2**64)
790+
self.assertRaises(TypeError, win.addch, 'a', 'bold')
787791
# A string method rejects a non-string, non-bytes argument.
788792
self.assertRaises(TypeError, win.addstr, 5)
789793
self.assertRaises(TypeError, win.addstr)
@@ -939,6 +943,11 @@ def test_attributes(self):
939943
self.assertRaises(OverflowError, win.attr_set, -1)
940944
self.assertRaises(OverflowError, win.attr_on, -1)
941945
self.assertRaises(OverflowError, win.attr_set, 1 << 64)
946+
# attron()/attroff()/attrset() reject a bad attribute too.
947+
self.assertRaises(OverflowError, win.attron, 1 << 64)
948+
self.assertRaises(OverflowError, win.attroff, -1)
949+
self.assertRaises(OverflowError, win.attrset, 1 << 64)
950+
self.assertRaises(TypeError, win.attron, 'x')
942951

943952
@requires_colors
944953
def test_attr_color_pair(self):
@@ -1630,6 +1639,11 @@ def test_color_attrs(self):
16301639
self.assertEqual(curses.pair_number(attr | curses.A_BOLD), pair)
16311640
self.assertEqual(curses.color_pair(0), 0)
16321641
self.assertEqual(curses.pair_number(0), 0)
1642+
# A pair too large to fit is rejected, not silently masked (gh-119138).
1643+
max_pair = curses.pair_number(curses.A_COLOR)
1644+
self.assertEqual(curses.pair_number(curses.color_pair(max_pair)), max_pair)
1645+
self.assertRaises(OverflowError, curses.color_pair, max_pair + 1)
1646+
self.assertRaises(OverflowError, curses.color_pair, -1)
16331647

16341648
@requires_curses_func('use_default_colors')
16351649
@requires_colors
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
The :mod:`curses` module now raises :exc:`OverflowError` instead of silently
2+
truncating an out-of-range value: :func:`curses.color_pair` rejects a color
3+
pair number that does not fit in the ``chtype`` color field, and the
4+
``attr`` argument of the character-cell and attribute methods
5+
(:meth:`~curses.window.addch`, :meth:`~curses.window.addstr`,
6+
:meth:`~curses.window.attron`, :meth:`~curses.window.attrset` and others) is
7+
checked against the ``chtype`` range.

Modules/_cursesmodule.c

Lines changed: 58 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ typedef struct {
692692
obj may also be a complexchar, whose cell is used directly; it carries its
693693
own rendition, so supplying *attr* too (attr_given) is rejected. */
694694
static int
695-
PyCurses_ConvertToCell(PyCursesWindowObject *win, PyObject *obj, long attr,
695+
PyCurses_ConvertToCell(PyCursesWindowObject *win, PyObject *obj, attr_t attr,
696696
int attr_given, const char *funcname,
697697
chtype *pch, cchar_t *pwc)
698698
{
@@ -1835,7 +1835,7 @@ _curses.window.addch
18351835
Character to add.
18361836
18371837
[
1838-
attr: long
1838+
attr: attr
18391839
Attributes for the character.
18401840
]
18411841
/
@@ -1851,8 +1851,8 @@ current settings for the window object.
18511851
static PyObject *
18521852
_curses_window_addch_impl(PyCursesWindowObject *self, int group_left_1,
18531853
int y, int x, PyObject *ch, int group_right_1,
1854-
long attr)
1855-
/*[clinic end generated code: output=00f4c37af3378f45 input=ab196a1dac3d354c]*/
1854+
attr_t attr)
1855+
/*[clinic end generated code: output=3306e15a7059998f input=0a09ecdd04aa0a2d]*/
18561856
{
18571857
int coordinates_group = group_left_1;
18581858
int rtn;
@@ -1908,7 +1908,7 @@ _curses_window_addch_impl(PyCursesWindowObject *self, int group_left_1,
19081908
#endif
19091909

19101910
static int
1911-
curses_wattrset(PyCursesWindowObject *self, long attr, const char *funcname)
1911+
curses_wattrset(PyCursesWindowObject *self, attr_t attr, const char *funcname)
19121912
{
19131913
if (wattrset(self->win, attr) == ERR) {
19141914
curses_window_set_error(self, "wattrset", funcname);
@@ -1931,7 +1931,7 @@ _curses.window.addstr
19311931
String to add.
19321932
19331933
[
1934-
attr: long
1934+
attr: attr
19351935
Attributes for characters.
19361936
]
19371937
/
@@ -1947,8 +1947,8 @@ current settings for the window object.
19471947
static PyObject *
19481948
_curses_window_addstr_impl(PyCursesWindowObject *self, int group_left_1,
19491949
int y, int x, PyObject *str, int group_right_1,
1950-
long attr)
1951-
/*[clinic end generated code: output=65a928ea85ff3115 input=ff6cbb91448a22a3]*/
1950+
attr_t attr)
1951+
/*[clinic end generated code: output=4942cdb202012076 input=0202b09895bcb472]*/
19521952
{
19531953
int rtn;
19541954
int strtype;
@@ -2046,7 +2046,7 @@ _curses.window.addnstr
20462046
Maximal number of characters.
20472047
20482048
[
2049-
attr: long
2049+
attr: attr
20502050
Attributes for characters.
20512051
]
20522052
/
@@ -2062,8 +2062,8 @@ current settings for the window object.
20622062
static PyObject *
20632063
_curses_window_addnstr_impl(PyCursesWindowObject *self, int group_left_1,
20642064
int y, int x, PyObject *str, int n,
2065-
int group_right_1, long attr)
2066-
/*[clinic end generated code: output=6d21cee2ce6876d9 input=72718415c2744a2a]*/
2065+
int group_right_1, attr_t attr)
2066+
/*[clinic end generated code: output=356ce38504dabf1d input=147405505606cd08]*/
20672067
{
20682068
int rtn;
20692069
int strtype;
@@ -2146,7 +2146,7 @@ _curses.window.bkgd
21462146
ch: object
21472147
Background character.
21482148
[
2149-
attr: long
2149+
attr: attr
21502150
Background attributes.
21512151
]
21522152
/
@@ -2156,8 +2156,8 @@ Set the background property of the window.
21562156

21572157
static PyObject *
21582158
_curses_window_bkgd_impl(PyCursesWindowObject *self, PyObject *ch,
2159-
int group_right_1, long attr)
2160-
/*[clinic end generated code: output=73cb11ecca59612f input=a2129c1b709db432]*/
2159+
int group_right_1, attr_t attr)
2160+
/*[clinic end generated code: output=4dc2599da3afa46a input=7aee8008ff8066a5]*/
21612161
{
21622162
chtype bkgd;
21632163
#ifdef HAVE_NCURSESW
@@ -2183,15 +2183,15 @@ _curses_window_bkgd_impl(PyCursesWindowObject *self, PyObject *ch,
21832183
/*[clinic input]
21842184
_curses.window.attroff
21852185
2186-
attr: long
2186+
attr: attr
21872187
/
21882188
21892189
Remove attribute attr from the "background" set.
21902190
[clinic start generated code]*/
21912191

21922192
static PyObject *
2193-
_curses_window_attroff_impl(PyCursesWindowObject *self, long attr)
2194-
/*[clinic end generated code: output=8a2fcd4df682fc64 input=786beedf06a7befe]*/
2193+
_curses_window_attroff_impl(PyCursesWindowObject *self, attr_t attr)
2194+
/*[clinic end generated code: output=27c9e77df32fa5d3 input=a22d4035e962e9a7]*/
21952195
{
21962196
int rtn = wattroff(self->win, (attr_t)attr);
21972197
return curses_window_check_err(self, rtn, "wattroff", "attroff");
@@ -2200,15 +2200,15 @@ _curses_window_attroff_impl(PyCursesWindowObject *self, long attr)
22002200
/*[clinic input]
22012201
_curses.window.attron
22022202
2203-
attr: long
2203+
attr: attr
22042204
/
22052205
22062206
Add attribute attr to the "background" set.
22072207
[clinic start generated code]*/
22082208

22092209
static PyObject *
2210-
_curses_window_attron_impl(PyCursesWindowObject *self, long attr)
2211-
/*[clinic end generated code: output=7afea43b237fa870 input=b57f824e1bf58326]*/
2210+
_curses_window_attron_impl(PyCursesWindowObject *self, attr_t attr)
2211+
/*[clinic end generated code: output=150ff7c387068cc7 input=361b6389f4d08681]*/
22122212
{
22132213
int rtn = wattron(self->win, (attr_t)attr);
22142214
return curses_window_check_err(self, rtn, "wattron", "attron");
@@ -2217,15 +2217,15 @@ _curses_window_attron_impl(PyCursesWindowObject *self, long attr)
22172217
/*[clinic input]
22182218
_curses.window.attrset
22192219
2220-
attr: long
2220+
attr: attr
22212221
/
22222222
22232223
Set the "background" set of attributes.
22242224
[clinic start generated code]*/
22252225

22262226
static PyObject *
2227-
_curses_window_attrset_impl(PyCursesWindowObject *self, long attr)
2228-
/*[clinic end generated code: output=84e379bff20c0433 input=42e400c0d0154ab5]*/
2227+
_curses_window_attrset_impl(PyCursesWindowObject *self, attr_t attr)
2228+
/*[clinic end generated code: output=1b57b2a512603eb0 input=af748b1c18e35c34]*/
22292229
{
22302230
int rtn = wattrset(self->win, (attr_t)attr);
22312231
return curses_window_check_err(self, rtn, "wattrset", "attrset");
@@ -2356,7 +2356,7 @@ _curses.window.bkgdset
23562356
ch: object
23572357
Background character.
23582358
[
2359-
attr: long
2359+
attr: attr
23602360
Background attributes.
23612361
]
23622362
/
@@ -2366,8 +2366,8 @@ Set the window's background.
23662366

23672367
static PyObject *
23682368
_curses_window_bkgdset_impl(PyCursesWindowObject *self, PyObject *ch,
2369-
int group_right_1, long attr)
2370-
/*[clinic end generated code: output=3c32f2de5685a482 input=1f0811b24af821ca]*/
2369+
int group_right_1, attr_t attr)
2370+
/*[clinic end generated code: output=32f5117c9e45422a input=64cf7cd3562b379b]*/
23712371
{
23722372
chtype bkgd;
23732373
#ifdef HAVE_NCURSESW
@@ -2729,7 +2729,7 @@ _curses.window.echochar
27292729
Character to add.
27302730
27312731
[
2732-
attr: long
2732+
attr: attr
27332733
Attributes for the character.
27342734
]
27352735
/
@@ -2739,8 +2739,8 @@ Add character ch with attribute attr, and refresh.
27392739

27402740
static PyObject *
27412741
_curses_window_echochar_impl(PyCursesWindowObject *self, PyObject *ch,
2742-
int group_right_1, long attr)
2743-
/*[clinic end generated code: output=f42da9e200c935e5 input=26e16855ec1b0e78]*/
2742+
int group_right_1, attr_t attr)
2743+
/*[clinic end generated code: output=ab03afa580aa6a2a input=cd74c42aadcc7e30]*/
27442744
{
27452745
chtype ch_;
27462746
#ifdef HAVE_NCURSESW
@@ -3167,7 +3167,7 @@ _curses.window.hline
31673167
Line length.
31683168
31693169
[
3170-
attr: long
3170+
attr: attr
31713171
Attributes for the characters.
31723172
]
31733173
/
@@ -3178,8 +3178,8 @@ Display a horizontal line.
31783178
static PyObject *
31793179
_curses_window_hline_impl(PyCursesWindowObject *self, int group_left_1,
31803180
int y, int x, PyObject *ch, int n,
3181-
int group_right_1, long attr)
3182-
/*[clinic end generated code: output=c00d489d61fc9eef input=924f8c28521bc2ec]*/
3181+
int group_right_1, attr_t attr)
3182+
/*[clinic end generated code: output=2c7489b8bd10c446 input=5d9f72ccba73975c]*/
31833183
{
31843184
chtype ch_;
31853185
#ifdef HAVE_NCURSESW
@@ -3223,7 +3223,7 @@ _curses.window.insch
32233223
Character to insert.
32243224
32253225
[
3226-
attr: long
3226+
attr: attr
32273227
Attributes for the character.
32283228
]
32293229
/
@@ -3237,8 +3237,8 @@ right, with the rightmost characters on the line being lost.
32373237
static PyObject *
32383238
_curses_window_insch_impl(PyCursesWindowObject *self, int group_left_1,
32393239
int y, int x, PyObject *ch, int group_right_1,
3240-
long attr)
3241-
/*[clinic end generated code: output=ade8cfe3a3bf3e34 input=47d2989159ae6ca7]*/
3240+
attr_t attr)
3241+
/*[clinic end generated code: output=9d2576c0d8d982c4 input=f76641d529dbd8af]*/
32423242
{
32433243
int rtn;
32443244
chtype ch_ = 0;
@@ -3571,7 +3571,7 @@ _curses.window.insstr
35713571
String to insert.
35723572
35733573
[
3574-
attr: long
3574+
attr: attr
35753575
Attributes for characters.
35763576
]
35773577
/
@@ -3588,8 +3588,8 @@ moving to y, x, if specified).
35883588
static PyObject *
35893589
_curses_window_insstr_impl(PyCursesWindowObject *self, int group_left_1,
35903590
int y, int x, PyObject *str, int group_right_1,
3591-
long attr)
3592-
/*[clinic end generated code: output=c259a5265ad0b777 input=dbfbdd3892155ea6]*/
3591+
attr_t attr)
3592+
/*[clinic end generated code: output=2c8ed843880619ab input=f4a9d26b270058c2]*/
35933593
{
35943594
int rtn;
35953595
int strtype;
@@ -3683,7 +3683,7 @@ _curses.window.insnstr
36833683
Maximal number of characters.
36843684
36853685
[
3686-
attr: long
3686+
attr: attr
36873687
Attributes for characters.
36883688
]
36893689
/
@@ -3701,8 +3701,8 @@ does not change (after moving to y, x, if specified).
37013701
static PyObject *
37023702
_curses_window_insnstr_impl(PyCursesWindowObject *self, int group_left_1,
37033703
int y, int x, PyObject *str, int n,
3704-
int group_right_1, long attr)
3705-
/*[clinic end generated code: output=971a32ea6328ec8b input=fd0a9b65b84b385f]*/
3704+
int group_right_1, attr_t attr)
3705+
/*[clinic end generated code: output=4895829689f3bdd2 input=7412feb3910276bf]*/
37063706
{
37073707
int rtn;
37083708
int strtype;
@@ -4263,7 +4263,7 @@ _curses.window.vline
42634263
Line length.
42644264
42654265
[
4266-
attr: long
4266+
attr: attr
42674267
Attributes for the character.
42684268
]
42694269
/
@@ -4274,8 +4274,8 @@ Display a vertical line.
42744274
static PyObject *
42754275
_curses_window_vline_impl(PyCursesWindowObject *self, int group_left_1,
42764276
int y, int x, PyObject *ch, int n,
4277-
int group_right_1, long attr)
4278-
/*[clinic end generated code: output=287ad1cc8982217f input=1d4aa27ff0309bbc]*/
4277+
int group_right_1, attr_t attr)
4278+
/*[clinic end generated code: output=18efd3ea37bb04f6 input=e8678752623197a1]*/
42794279
{
42804280
chtype ch_;
42814281
#ifdef HAVE_NCURSESW
@@ -5228,7 +5228,20 @@ _curses_color_pair_impl(PyObject *module, int pair_number)
52285228
PyCursesStatefulInitialised(module);
52295229
PyCursesStatefulInitialisedColor(module);
52305230

5231-
return PyLong_FromLong(COLOR_PAIR(pair_number));
5231+
/* COLOR_PAIR() packs the pair into a limited field; a pair too large to be
5232+
recovered by its inverse PAIR_NUMBER() would be masked to a different
5233+
one. Reject pairs that do not round-trip (this assumes only that the two
5234+
macros are inverses). color_set()/attr_set()/complexchar can still
5235+
display larger pairs. */
5236+
chtype attr = COLOR_PAIR(pair_number);
5237+
if (pair_number < 0 || PAIR_NUMBER(attr) != pair_number) {
5238+
PyErr_Format(PyExc_OverflowError,
5239+
"color pair %d does not fit in a chtype "
5240+
"(color_pair() can encode only pairs 0 to %d)",
5241+
pair_number, (int)PAIR_NUMBER(A_COLOR));
5242+
return NULL;
5243+
}
5244+
return PyLong_FromLong(attr);
52325245
}
52335246

52345247
/*[clinic input]

0 commit comments

Comments
 (0)