Skip to content

Commit 7bf63fa

Browse files
gh-152275: Add integer overflow guards to the curses chtype and color-pair packing path (GH-152303)
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 3fa72d5 commit 7bf63fa

5 files changed

Lines changed: 139 additions & 104 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
@@ -814,6 +814,10 @@ def test_argument_errors(self):
814814
# A character argument must be an int, a byte or a one-element string.
815815
self.assertRaises(TypeError, win.addch, [])
816816
self.assertRaises(OverflowError, win.addch, 2**64)
817+
# The attribute argument is rejected, not truncated, when out of range.
818+
self.assertRaises(OverflowError, win.addch, 'a', 2**64)
819+
self.assertRaises(OverflowError, win.addstr, 'a', 2**64)
820+
self.assertRaises(TypeError, win.addch, 'a', 'bold')
817821
# A string method rejects a non-string, non-bytes argument.
818822
self.assertRaises(TypeError, win.addstr, 5)
819823
self.assertRaises(TypeError, win.addstr)
@@ -969,6 +973,11 @@ def test_attributes(self):
969973
self.assertRaises(OverflowError, win.attr_set, -1)
970974
self.assertRaises(OverflowError, win.attr_on, -1)
971975
self.assertRaises(OverflowError, win.attr_set, 1 << 64)
976+
# attron()/attroff()/attrset() reject a bad attribute too.
977+
self.assertRaises(OverflowError, win.attron, 1 << 64)
978+
self.assertRaises(OverflowError, win.attroff, -1)
979+
self.assertRaises(OverflowError, win.attrset, 1 << 64)
980+
self.assertRaises(TypeError, win.attron, 'x')
972981

973982
@requires_colors
974983
def test_attr_color_pair(self):
@@ -1717,6 +1726,11 @@ def test_color_attrs(self):
17171726
self.assertEqual(curses.pair_number(attr | curses.A_BOLD), pair)
17181727
self.assertEqual(curses.color_pair(0), 0)
17191728
self.assertEqual(curses.pair_number(0), 0)
1729+
# A pair too large to fit is rejected, not silently masked (gh-119138).
1730+
max_pair = curses.pair_number(curses.A_COLOR)
1731+
self.assertEqual(curses.pair_number(curses.color_pair(max_pair)), max_pair)
1732+
self.assertRaises(OverflowError, curses.color_pair, max_pair + 1)
1733+
self.assertRaises(OverflowError, curses.color_pair, -1)
17201734

17211735
@requires_curses_func('use_default_colors')
17221736
@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: 60 additions & 46 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
{
@@ -931,8 +931,9 @@ attr_converter(PyObject *arg, void *ptr)
931931
class attr_converter(CConverter):
932932
type = 'attr_t'
933933
converter = 'attr_converter'
934+
c_ignored_default = '0'
934935
[python start generated code]*/
935-
/*[python end generated code: output=da39a3ee5e6b4b0d input=6132d3d99d3ec25a]*/
936+
/*[python end generated code: output=da39a3ee5e6b4b0d input=57b994c97cbd5e80]*/
936937

937938
#ifdef HAVE_NCURSESW
938939
/* -------------------------------------------------------*/
@@ -1835,7 +1836,7 @@ _curses.window.addch
18351836
Character to add.
18361837
18371838
[
1838-
attr: long
1839+
attr: attr
18391840
Attributes for the character.
18401841
]
18411842
/
@@ -1851,8 +1852,8 @@ current settings for the window object.
18511852
static PyObject *
18521853
_curses_window_addch_impl(PyCursesWindowObject *self, int group_left_1,
18531854
int y, int x, PyObject *ch, int group_right_1,
1854-
long attr)
1855-
/*[clinic end generated code: output=00f4c37af3378f45 input=ab196a1dac3d354c]*/
1855+
attr_t attr)
1856+
/*[clinic end generated code: output=3306e15a7059998f input=0a09ecdd04aa0a2d]*/
18561857
{
18571858
int coordinates_group = group_left_1;
18581859
int rtn;
@@ -1908,7 +1909,7 @@ _curses_window_addch_impl(PyCursesWindowObject *self, int group_left_1,
19081909
#endif
19091910

19101911
static int
1911-
curses_wattrset(PyCursesWindowObject *self, long attr, const char *funcname)
1912+
curses_wattrset(PyCursesWindowObject *self, attr_t attr, const char *funcname)
19121913
{
19131914
if (wattrset(self->win, attr) == ERR) {
19141915
curses_window_set_error(self, "wattrset", funcname);
@@ -1931,7 +1932,7 @@ _curses.window.addstr
19311932
String to add.
19321933
19331934
[
1934-
attr: long
1935+
attr: attr
19351936
Attributes for characters.
19361937
]
19371938
/
@@ -1947,8 +1948,8 @@ current settings for the window object.
19471948
static PyObject *
19481949
_curses_window_addstr_impl(PyCursesWindowObject *self, int group_left_1,
19491950
int y, int x, PyObject *str, int group_right_1,
1950-
long attr)
1951-
/*[clinic end generated code: output=65a928ea85ff3115 input=ff6cbb91448a22a3]*/
1951+
attr_t attr)
1952+
/*[clinic end generated code: output=4942cdb202012076 input=0202b09895bcb472]*/
19521953
{
19531954
int rtn;
19541955
int strtype;
@@ -2046,7 +2047,7 @@ _curses.window.addnstr
20462047
Maximal number of characters.
20472048
20482049
[
2049-
attr: long
2050+
attr: attr
20502051
Attributes for characters.
20512052
]
20522053
/
@@ -2062,8 +2063,8 @@ current settings for the window object.
20622063
static PyObject *
20632064
_curses_window_addnstr_impl(PyCursesWindowObject *self, int group_left_1,
20642065
int y, int x, PyObject *str, int n,
2065-
int group_right_1, long attr)
2066-
/*[clinic end generated code: output=6d21cee2ce6876d9 input=72718415c2744a2a]*/
2066+
int group_right_1, attr_t attr)
2067+
/*[clinic end generated code: output=356ce38504dabf1d input=147405505606cd08]*/
20672068
{
20682069
int rtn;
20692070
int strtype;
@@ -2146,7 +2147,7 @@ _curses.window.bkgd
21462147
ch: object
21472148
Background character.
21482149
[
2149-
attr: long
2150+
attr: attr
21502151
Background attributes.
21512152
]
21522153
/
@@ -2156,8 +2157,8 @@ Set the background property of the window.
21562157

21572158
static PyObject *
21582159
_curses_window_bkgd_impl(PyCursesWindowObject *self, PyObject *ch,
2159-
int group_right_1, long attr)
2160-
/*[clinic end generated code: output=73cb11ecca59612f input=a2129c1b709db432]*/
2160+
int group_right_1, attr_t attr)
2161+
/*[clinic end generated code: output=4dc2599da3afa46a input=7aee8008ff8066a5]*/
21612162
{
21622163
chtype bkgd;
21632164
#ifdef HAVE_NCURSESW
@@ -2183,15 +2184,15 @@ _curses_window_bkgd_impl(PyCursesWindowObject *self, PyObject *ch,
21832184
/*[clinic input]
21842185
_curses.window.attroff
21852186
2186-
attr: long
2187+
attr: attr
21872188
/
21882189
21892190
Remove attribute attr from the "background" set.
21902191
[clinic start generated code]*/
21912192

21922193
static PyObject *
2193-
_curses_window_attroff_impl(PyCursesWindowObject *self, long attr)
2194-
/*[clinic end generated code: output=8a2fcd4df682fc64 input=786beedf06a7befe]*/
2194+
_curses_window_attroff_impl(PyCursesWindowObject *self, attr_t attr)
2195+
/*[clinic end generated code: output=27c9e77df32fa5d3 input=a22d4035e962e9a7]*/
21952196
{
21962197
int rtn = wattroff(self->win, (attr_t)attr);
21972198
return curses_window_check_err(self, rtn, "wattroff", "attroff");
@@ -2200,15 +2201,15 @@ _curses_window_attroff_impl(PyCursesWindowObject *self, long attr)
22002201
/*[clinic input]
22012202
_curses.window.attron
22022203
2203-
attr: long
2204+
attr: attr
22042205
/
22052206
22062207
Add attribute attr to the "background" set.
22072208
[clinic start generated code]*/
22082209

22092210
static PyObject *
2210-
_curses_window_attron_impl(PyCursesWindowObject *self, long attr)
2211-
/*[clinic end generated code: output=7afea43b237fa870 input=b57f824e1bf58326]*/
2211+
_curses_window_attron_impl(PyCursesWindowObject *self, attr_t attr)
2212+
/*[clinic end generated code: output=150ff7c387068cc7 input=361b6389f4d08681]*/
22122213
{
22132214
int rtn = wattron(self->win, (attr_t)attr);
22142215
return curses_window_check_err(self, rtn, "wattron", "attron");
@@ -2217,15 +2218,15 @@ _curses_window_attron_impl(PyCursesWindowObject *self, long attr)
22172218
/*[clinic input]
22182219
_curses.window.attrset
22192220
2220-
attr: long
2221+
attr: attr
22212222
/
22222223
22232224
Set the "background" set of attributes.
22242225
[clinic start generated code]*/
22252226

22262227
static PyObject *
2227-
_curses_window_attrset_impl(PyCursesWindowObject *self, long attr)
2228-
/*[clinic end generated code: output=84e379bff20c0433 input=42e400c0d0154ab5]*/
2228+
_curses_window_attrset_impl(PyCursesWindowObject *self, attr_t attr)
2229+
/*[clinic end generated code: output=1b57b2a512603eb0 input=af748b1c18e35c34]*/
22292230
{
22302231
int rtn = wattrset(self->win, (attr_t)attr);
22312232
return curses_window_check_err(self, rtn, "wattrset", "attrset");
@@ -2356,7 +2357,7 @@ _curses.window.bkgdset
23562357
ch: object
23572358
Background character.
23582359
[
2359-
attr: long
2360+
attr: attr
23602361
Background attributes.
23612362
]
23622363
/
@@ -2366,8 +2367,8 @@ Set the window's background.
23662367

23672368
static PyObject *
23682369
_curses_window_bkgdset_impl(PyCursesWindowObject *self, PyObject *ch,
2369-
int group_right_1, long attr)
2370-
/*[clinic end generated code: output=3c32f2de5685a482 input=1f0811b24af821ca]*/
2370+
int group_right_1, attr_t attr)
2371+
/*[clinic end generated code: output=32f5117c9e45422a input=64cf7cd3562b379b]*/
23712372
{
23722373
chtype bkgd;
23732374
#ifdef HAVE_NCURSESW
@@ -2756,7 +2757,7 @@ _curses.window.echochar
27562757
Character to add.
27572758
27582759
[
2759-
attr: long
2760+
attr: attr
27602761
Attributes for the character.
27612762
]
27622763
/
@@ -2766,8 +2767,8 @@ Add character ch with attribute attr, and refresh.
27662767

27672768
static PyObject *
27682769
_curses_window_echochar_impl(PyCursesWindowObject *self, PyObject *ch,
2769-
int group_right_1, long attr)
2770-
/*[clinic end generated code: output=f42da9e200c935e5 input=26e16855ec1b0e78]*/
2770+
int group_right_1, attr_t attr)
2771+
/*[clinic end generated code: output=ab03afa580aa6a2a input=cd74c42aadcc7e30]*/
27712772
{
27722773
chtype ch_;
27732774
#ifdef HAVE_NCURSESW
@@ -3194,7 +3195,7 @@ _curses.window.hline
31943195
Line length.
31953196
31963197
[
3197-
attr: long
3198+
attr: attr
31983199
Attributes for the characters.
31993200
]
32003201
/
@@ -3205,8 +3206,8 @@ Display a horizontal line.
32053206
static PyObject *
32063207
_curses_window_hline_impl(PyCursesWindowObject *self, int group_left_1,
32073208
int y, int x, PyObject *ch, int n,
3208-
int group_right_1, long attr)
3209-
/*[clinic end generated code: output=c00d489d61fc9eef input=924f8c28521bc2ec]*/
3209+
int group_right_1, attr_t attr)
3210+
/*[clinic end generated code: output=2c7489b8bd10c446 input=5d9f72ccba73975c]*/
32103211
{
32113212
chtype ch_;
32123213
#ifdef HAVE_NCURSESW
@@ -3250,7 +3251,7 @@ _curses.window.insch
32503251
Character to insert.
32513252
32523253
[
3253-
attr: long
3254+
attr: attr
32543255
Attributes for the character.
32553256
]
32563257
/
@@ -3264,8 +3265,8 @@ right, with the rightmost characters on the line being lost.
32643265
static PyObject *
32653266
_curses_window_insch_impl(PyCursesWindowObject *self, int group_left_1,
32663267
int y, int x, PyObject *ch, int group_right_1,
3267-
long attr)
3268-
/*[clinic end generated code: output=ade8cfe3a3bf3e34 input=47d2989159ae6ca7]*/
3268+
attr_t attr)
3269+
/*[clinic end generated code: output=9d2576c0d8d982c4 input=f76641d529dbd8af]*/
32693270
{
32703271
int rtn;
32713272
chtype ch_ = 0;
@@ -3598,7 +3599,7 @@ _curses.window.insstr
35983599
String to insert.
35993600
36003601
[
3601-
attr: long
3602+
attr: attr
36023603
Attributes for characters.
36033604
]
36043605
/
@@ -3615,8 +3616,8 @@ moving to y, x, if specified).
36153616
static PyObject *
36163617
_curses_window_insstr_impl(PyCursesWindowObject *self, int group_left_1,
36173618
int y, int x, PyObject *str, int group_right_1,
3618-
long attr)
3619-
/*[clinic end generated code: output=c259a5265ad0b777 input=dbfbdd3892155ea6]*/
3619+
attr_t attr)
3620+
/*[clinic end generated code: output=2c8ed843880619ab input=f4a9d26b270058c2]*/
36203621
{
36213622
int rtn;
36223623
int strtype;
@@ -3710,7 +3711,7 @@ _curses.window.insnstr
37103711
Maximal number of characters.
37113712
37123713
[
3713-
attr: long
3714+
attr: attr
37143715
Attributes for characters.
37153716
]
37163717
/
@@ -3728,8 +3729,8 @@ does not change (after moving to y, x, if specified).
37283729
static PyObject *
37293730
_curses_window_insnstr_impl(PyCursesWindowObject *self, int group_left_1,
37303731
int y, int x, PyObject *str, int n,
3731-
int group_right_1, long attr)
3732-
/*[clinic end generated code: output=971a32ea6328ec8b input=fd0a9b65b84b385f]*/
3732+
int group_right_1, attr_t attr)
3733+
/*[clinic end generated code: output=4895829689f3bdd2 input=7412feb3910276bf]*/
37333734
{
37343735
int rtn;
37353736
int strtype;
@@ -4290,7 +4291,7 @@ _curses.window.vline
42904291
Line length.
42914292
42924293
[
4293-
attr: long
4294+
attr: attr
42944295
Attributes for the character.
42954296
]
42964297
/
@@ -4301,8 +4302,8 @@ Display a vertical line.
43014302
static PyObject *
43024303
_curses_window_vline_impl(PyCursesWindowObject *self, int group_left_1,
43034304
int y, int x, PyObject *ch, int n,
4304-
int group_right_1, long attr)
4305-
/*[clinic end generated code: output=287ad1cc8982217f input=1d4aa27ff0309bbc]*/
4305+
int group_right_1, attr_t attr)
4306+
/*[clinic end generated code: output=18efd3ea37bb04f6 input=e8678752623197a1]*/
43064307
{
43074308
chtype ch_;
43084309
#ifdef HAVE_NCURSESW
@@ -5272,7 +5273,20 @@ _curses_color_pair_impl(PyObject *module, int pair_number)
52725273
PyCursesStatefulInitialised(module);
52735274
PyCursesStatefulInitialisedColor(module);
52745275

5275-
return PyLong_FromLong(COLOR_PAIR(pair_number));
5276+
/* COLOR_PAIR() packs the pair into a limited field; a pair too large to be
5277+
recovered by its inverse PAIR_NUMBER() would be masked to a different
5278+
one. Reject pairs that do not round-trip (this assumes only that the two
5279+
macros are inverses). color_set()/attr_set()/complexchar can still
5280+
display larger pairs. */
5281+
chtype attr = COLOR_PAIR(pair_number);
5282+
if (pair_number < 0 || PAIR_NUMBER(attr) != pair_number) {
5283+
PyErr_Format(PyExc_OverflowError,
5284+
"color pair %d does not fit in a chtype "
5285+
"(color_pair() can encode only pairs 0 to %d)",
5286+
pair_number, (int)PAIR_NUMBER(A_COLOR));
5287+
return NULL;
5288+
}
5289+
return PyLong_FromLong(attr);
52765290
}
52775291

52785292
/*[clinic input]

0 commit comments

Comments
 (0)